Tux the Penguipedia tuxthepenguinwiki https://tuxthepenguin.miraheze.org/wiki/Main_Page MediaWiki 1.40.1 first-letter Media Special Talk User User talk Tux the Penguipedia Tux the Penguipedia talk File File talk MediaWiki MediaWiki talk Template Template talk Help Help talk Category Category talk Form Form talk Video Video talk Campaign Campaign talk User blog User blog talk TimedText TimedText talk Module Module talk Gadget Gadget talk Gadget definition Gadget definition talk Topic Navigation Navigation talk Module:Reply to 828 88 168 2020-11-17T18:27:57Z dev>BrandonWM 0 Created page with "local p = {} local function makeError(msg) msg ='Error in [[Template:Reply to]]: ' .. msg return mw.text.tag('strong', {['class']='error'}, msg) end function p.replyto(fra..." Scribunto text/plain local p = {} local function makeError(msg) msg ='Error in [[Template:Reply to]]: ' .. msg return mw.text.tag('strong', {['class']='error'}, msg) end function p.replyto(frame) local origArgs = frame:getParent().args local args = {} local maxArg = 1 local usernames = 0 for k, v in pairs(origArgs) do if type(k) == 'number' then if mw.ustring.match(v,'%S') then if k > maxArg then maxArg = k end usernames = usernames + 1 local title = mw.title.new(v) if not title then return makeError('Input contains forbidden characters.') end args[k] = title.rootText end elseif v == '' and k:sub(0,5) == 'label' then args[k] = '&#x200B;' else args[k] = v end end if usernames > (tonumber(frame.args.max) or 50) then return makeError(string.format( 'More than %s names specified.', tostring(frame.args.max or 50) )) else if usernames < 1 then if frame.args.example then args[1] = frame.args.example else return makeError('Username not given.') end end args['label1'] = args['label1'] or args['label'] local isfirst = true local outStr = args['prefix'] or '@' for i = 1, maxArg do if args[i] then if isfirst then isfirst = false else if ( (usernames > 2) or ((usernames == 2) and (args['c'] == '')) ) then outStr = outStr..', ' end if i == maxArg then outStr = outStr..' '..(args['c'] or 'and') .. ' ' end end outStr = string.format( '%s[[User:%s|%s]]', outStr, args[i], args['label'..tostring(i)] or args[i] ) end end outStr = outStr..(args['p'] or ':') return mw.text.tag('span', {['class']='template-ping'}, outStr) end end return p 14f0cd73a8a9f122c0e0e15382219083c602c62a Template:Not done 10 28 49 2020-11-17T19:23:53Z dev>MacFan4000 0 9 revisions imported from [[:templatewiki:Template:Not_done]] wikitext text/x-wiki [[File:X mark.svg|18px]] '''{{{1|Not done}}}'''<noinclude> {{documentation}} [[Category:Resolution templates]] </noinclude> fd025e245bee74ddd6c5ae757f983a3cd3258d94 Template:Doing 10 26 45 2020-11-17T19:24:11Z dev>MacFan4000 0 46 revisions imported from [[:templatewiki:Template:Doing]] wikitext text/x-wiki [[File:Pictogram voting wait.svg|18px|link=|alt=]]&nbsp;'''{{{1|Doing…}}}'''<noinclude> {{documentation}} [[Category:Resolution templates]] </noinclude> eb1feacb3d7a14829c6a9be311732ee0d24d35f3 Template:Clear 10 36 64 2022-03-08T14:12:32Z dev>DarkMatterMan4500 0 Created page with "<div style="clear:{{{1|both}}};"></div><noinclude> {{documentation}} </noinclude>" wikitext text/x-wiki <div style="clear:{{{1|both}}};"></div><noinclude> {{documentation}} </noinclude> 38bab3e3d7fbd3d6800d46556e60bc6bac494d72 Template:Talk quote inline 10 54 100 2022-09-30T00:21:50Z dev>Pppery 0 Oops wikitext text/x-wiki <templatestyles src="Talk quote inline/styles.css" /><!-- --><q {{#if: {{{title|}}} | title="{{{title}}}"}} class="inline-quote-talk {{#if: {{{i|{{{italic|}}}}}} | inline-quote-talk-italic}} {{#if: {{{q|{{{quotes|}}}}}}|inline-quote-talk-marks}}">{{{1|Example text}}}</q><!-- --><noinclude> {{Documentation}} </noinclude> b18e2fdc57277adbf4e3f4f513e78ecc5831453f Template:Comment 10 25 43 2022-09-30T00:50:50Z dev>Pppery 0 9 revisions imported from [[:meta:Template:Comment]] wikitext text/x-wiki [[File:Pictogram voting comment.svg|18px|link=]]&nbsp;'''{{{1|Comment:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude> 5f48a21f6ec3dc6d82cfa7a668c9e00fec175396 Template:Information 10 52 96 2022-09-30T01:03:49Z dev>Pppery 0 wikitext text/x-wiki <templatestyles src="Information/style.css" /> <div class="hproduct commons-file-information-table"> <table class="toccolours vevent fileinfotpl-type-information" style="width: 100%;" cellpadding="4"> <!-- Description --> <tr style="vertical-align: top"> <td id="fileinfotpl_desc" class="fileinfo-paramfield">Description<span class="summary fn" style="display:none">{{PAGENAME}}</span></td> <td class="description">{{ #if: {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{Description missing}} }}</td> </tr> <!-- Source --> <tr style="vertical-align: top"> <td id="fileinfotpl_src" class="fileinfo-paramfield">Source</td> <td>{{ #if: {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{Description missing|source information}} }}</td> </tr> <!-- Author --> <tr style="vertical-align: top"> <td id="fileinfotpl_aut" class="fileinfo-paramfield">Author</td> <td>{{ #if: {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{Description missing|author information}} }}</td> </tr> <!-- Fecha --> <tr style="vertical-align: top"> <td id="fileinfotpl_aut" class="fileinfo-paramfield">Date</td> <td>{{{date|{{{Date|{{{fecha|{{{Fecha|}}}}}}}}}}}}</td> </tr> <!-- Other versions --> {{#switch: {{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}}{{{demo|<noinclude>1</noinclude>}}} | = | - = | none = | #default = <tr style="vertical-align: top"> <td id="fileinfotpl_ver" class="fileinfo-paramfield" style="background: #ccf; text-align: right; padding-right: 0.4em; width: 15%; font-weight:bold">Other versions</td> <td> {{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}} </td> </tr> }} </table> </div><noinclude>{{Documentation}}</noinclude> 3a749131ebeff40d99673728a045f76f81456cc1 Template:On hold 10 37 66 2022-09-30T01:06:11Z dev>Pppery 0 Pppery moved page [[Template:On Hold]] to [[Template:On hold]] wikitext text/x-wiki [[File:Symbol wait.svg|18px]] '''{{{1|On hold}}}'''<noinclude>[[Category:Resolution templates]]<noinclude> {{documentation}} [[Category:Resolution templates]]</noinclude> 7a18e8aa8c80a33b1a68eed60d0993a75202162f Template:Template link 10 74 140 2022-09-30T01:10:00Z dev>Pppery 0 46 revisions imported from [[:wikipedia:Template:Template_link]] wikitext text/x-wiki &#123;&#123;[[Template:{{{1}}}|{{{1}}}]]&#125;&#125;<noinclude>{{documentation}} <!-- Categories go on the /doc subpage and interwikis go on Wikidata. --> </noinclude> eabbec62efe3044a98ebb3ce9e7d4d43c222351d Template:Abstain 10 60 112 2022-09-30T01:41:43Z dev>Pppery 0 Add doc wikitext text/x-wiki [[File:Symbol neutral vote.svg|18px]]&nbsp;'''<bdi>{{{1|Abstain}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> b1098b70832376165658562bfc8de5b6187bdb26 Template:Documentation 10 18 29 2022-09-30T01:43:37Z dev>MacFan4000 0 4 revisions imported from [[:meta:Template:Documentation]]: this is useful and was on templateiwki wikitext text/x-wiki {{#invoke:documentation|main|_content={{ {{#invoke:documentation|contentTitle}}}}}}<noinclude>[[Category:Templates]]</noinclude> 9885bb4fa99bf3d5b960e73606bbb8eed3026877 Template:User wikimedia 10 67 126 2022-09-30T01:44:10Z dev>MacFan4000 0 9 revisions imported from [[:meta:Template:User_wikimedia]]: this is useful and was on templateiwki wikitext text/x-wiki {{Userbox | id = [[File:Wikimedia Foundation Logo.png|43px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an [[metawikimedia:Special:CentralAuth/{{{account|{{BASEPAGENAME}}}}}|account]] at the Wikimedia Foundation projects. | nocat = {{{nocat|}}} | usercategory = Wikimedians }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> c7b06f2b4d088ee94d893eb1e3548e9e0562fc5e Template:Neutral 10 61 114 2022-09-30T01:53:01Z dev>Pppery 0 Add doc wikitext text/x-wiki [[File:Symbol neutral vote.svg|18px]]&nbsp;'''<bdi>{{{1|Neutral}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> 59552c46cb01ccf2c6196bdea9ec3eb90858e675 Template:Oppose 10 62 116 2022-09-30T01:54:33Z dev>Pppery 0 Add doc wikitext text/x-wiki {{ #switch: {{{4|{{{1|}}}}}} | Regular= [[File:Symbol oppose vote.png|18px|alt=]] | Normal= [[File:Symbol oppose vote.png|18px|alt=]] | Strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | Strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | Strongest = [[File:Symbol full oppose vote.svg|20px|alt=]] | Weak= [[File:Weak Oppose.png|18px|alt=]] | Weakly= [[File:Weak Oppose.png|18px|alt=]] | strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | weak= [[File:Weak Oppose.png|18px|alt=]] | weakly= [[File:Weak Oppose.png|18px|alt=]] | strongest = [[File:Symbol full oppose vote.svg|20px|alt=]] |#default= [[File:Symbol oppose vote.svg|18px|alt=]] }} {{ #switch: {{{1|}}} | Regular='''Oppose''' | Normal= '''Oppose''' | Strongest = '''Strongest oppose''' | Strongly= '''Strongly oppose''' | Strong= '''Strong oppose''' | Weak= '''Weak oppose''' | Weakly= '''Weakly oppose''' | strongly= '''Strongly oppose''' | strong= '''Strong oppose''' | weak= '''Weak oppose''' | weakly= '''Weakly oppose''' | strongest = '''Strongest oppose''' | {{{other|2}}} = '''{{{3}}}''' |#default= '''Oppose''' }}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> 8c57115a1c36446a717d2c874b2895b057d1ffc3 Template:Ping 10 59 110 2022-09-30T02:01:54Z dev>Pppery 0 wikitext text/x-wiki {{{{{|safesubst:}}}#invoke:Reply to|replyto|<noinclude>example=Example</noinclude>|max=50}}<noinclude>{{documentation}}</noinclude> 0a7b3547181e17a03ec99855e276688fcc36ce1e Template:Support 10 63 118 2022-09-30T02:02:35Z dev>Pppery 0 Add doc wikitext text/x-wiki {{ #switch: {{{4|{{{1|}}}}}} | Regular= [[File:Symbol support vote.svg|18px|alt=]] | Normal= [[File:Symbol support vote.svg|18px|alt=]] | Strongly= [[File:Symbol strong support vote.svg|18px|alt=]] | Strongest= [[File:Symbol full support vote.svg|22px]] | Strong= [[File:Symbol strong support vote.svg|18px|alt=]] | Weak= [[File:Symbol partial support vote.svg|18px|alt=]] | Weakly= [[File:Symbol partial support vote.svg|18px|alt=]] | strongly= [[File:Symbol strong support vote.svg|18px|alt=]] | strong= [[File:Symbol strong support vote.svg|18px|alt=]] | weak= [[File:Symbol partial support vote.svg|18px|alt=]] | weakly= [[File:Symbol partial support vote.svg|18px|alt=]] |#default= [[File:Symbol support vote.svg|18px|alt=]] }} {{ #switch: {{{1|}}} | Regular='''Support''' | Normal= '''Support''' | Strongly= '''Strongly support''' | Strong= '''Strong support''' | Weak= '''Weak support''' | Weakly= '''Weakly support''' | strongly= '''Strongly support''' | Strongest= '''''Strongest support''''' | strong= '''Strong support''' | weak= '''Weak support''' | weakly= '''Weakly support''' | {{{other|2}}} = '''{{{3}}}''' |#default= '''Support''' }}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> 0a43c05b804693f20b74446f7e7e6d7ccd10c516 Template:= 10 57 106 2022-09-30T02:18:15Z dev>Pppery 0 Replaced content with "=<noinclude> {{documentation}} </noinclude>" wikitext text/x-wiki =<noinclude> {{documentation}} </noinclude> 44f3105df6073eb65369938814d1551b51611402 Template:User discord 10 66 124 2022-09-30T02:26:18Z dev>Pppery 0 wikitext text/x-wiki {{Userbox | id = # | id-s = 24 | id-fc = #5865F2 | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:Discord|Discord]]{{#if:{{{account|}}}|&#32;as ''{{{account}}}''|}}. | nocat = {{{nocat|}}} | usercategory = Users who use Discord }}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> 19b1d90000718152b9058c16c7c1ba13d7cb2715 Module:Arguments 828 21 35 2022-09-30T02:32:01Z dev>Pppery 0 24 revisions imported from [[:wikipedia:Module:Arguments]] Scribunto text/plain -- This module provides easy processing of arguments passed to Scribunto from -- #invoke. It is intended for use by other Lua modules, and should not be -- called from #invoke directly. local libraryUtil = require('libraryUtil') local checkType = libraryUtil.checkType local arguments = {} -- Generate four different tidyVal functions, so that we don't have to check the -- options every time we call it. local function tidyValDefault(key, val) if type(val) == 'string' then val = val:match('^%s*(.-)%s*$') if val == '' then return nil else return val end else return val end end local function tidyValTrimOnly(key, val) if type(val) == 'string' then return val:match('^%s*(.-)%s*$') else return val end end local function tidyValRemoveBlanksOnly(key, val) if type(val) == 'string' then if val:find('%S') then return val else return nil end else return val end end local function tidyValNoChange(key, val) return val end local function matchesTitle(given, title) local tp = type( given ) return (tp == 'string' or tp == 'number') and mw.title.new( given ).prefixedText == title end local translate_mt = { __index = function(t, k) return k end } function arguments.getArgs(frame, options) checkType('getArgs', 1, frame, 'table', true) checkType('getArgs', 2, options, 'table', true) frame = frame or {} options = options or {} --[[ -- Set up argument translation. --]] options.translate = options.translate or {} if getmetatable(options.translate) == nil then setmetatable(options.translate, translate_mt) end if options.backtranslate == nil then options.backtranslate = {} for k,v in pairs(options.translate) do options.backtranslate[v] = k end end if options.backtranslate and getmetatable(options.backtranslate) == nil then setmetatable(options.backtranslate, { __index = function(t, k) if options.translate[k] ~= k then return nil else return k end end }) end --[[ -- Get the argument tables. If we were passed a valid frame object, get the -- frame arguments (fargs) and the parent frame arguments (pargs), depending -- on the options set and on the parent frame's availability. If we weren't -- passed a valid frame object, we are being called from another Lua module -- or from the debug console, so assume that we were passed a table of args -- directly, and assign it to a new variable (luaArgs). --]] local fargs, pargs, luaArgs if type(frame.args) == 'table' and type(frame.getParent) == 'function' then if options.wrappers then --[[ -- The wrappers option makes Module:Arguments look up arguments in -- either the frame argument table or the parent argument table, but -- not both. This means that users can use either the #invoke syntax -- or a wrapper template without the loss of performance associated -- with looking arguments up in both the frame and the parent frame. -- Module:Arguments will look up arguments in the parent frame -- if it finds the parent frame's title in options.wrapper; -- otherwise it will look up arguments in the frame object passed -- to getArgs. --]] local parent = frame:getParent() if not parent then fargs = frame.args else local title = parent:getTitle():gsub('/sandbox$', '') local found = false if matchesTitle(options.wrappers, title) then found = true elseif type(options.wrappers) == 'table' then for _,v in pairs(options.wrappers) do if matchesTitle(v, title) then found = true break end end end -- We test for false specifically here so that nil (the default) acts like true. if found or options.frameOnly == false then pargs = parent.args end if not found or options.parentOnly == false then fargs = frame.args end end else -- options.wrapper isn't set, so check the other options. if not options.parentOnly then fargs = frame.args end if not options.frameOnly then local parent = frame:getParent() pargs = parent and parent.args or nil end end if options.parentFirst then fargs, pargs = pargs, fargs end else luaArgs = frame end -- Set the order of precedence of the argument tables. If the variables are -- nil, nothing will be added to the table, which is how we avoid clashes -- between the frame/parent args and the Lua args. local argTables = {fargs} argTables[#argTables + 1] = pargs argTables[#argTables + 1] = luaArgs --[[ -- Generate the tidyVal function. If it has been specified by the user, we -- use that; if not, we choose one of four functions depending on the -- options chosen. This is so that we don't have to call the options table -- every time the function is called. --]] local tidyVal = options.valueFunc if tidyVal then if type(tidyVal) ~= 'function' then error( "bad value assigned to option 'valueFunc'" .. '(function expected, got ' .. type(tidyVal) .. ')', 2 ) end elseif options.trim ~= false then if options.removeBlanks ~= false then tidyVal = tidyValDefault else tidyVal = tidyValTrimOnly end else if options.removeBlanks ~= false then tidyVal = tidyValRemoveBlanksOnly else tidyVal = tidyValNoChange end end --[[ -- Set up the args, metaArgs and nilArgs tables. args will be the one -- accessed from functions, and metaArgs will hold the actual arguments. Nil -- arguments are memoized in nilArgs, and the metatable connects all of them -- together. --]] local args, metaArgs, nilArgs, metatable = {}, {}, {}, {} setmetatable(args, metatable) local function mergeArgs(tables) --[[ -- Accepts multiple tables as input and merges their keys and values -- into one table. If a value is already present it is not overwritten; -- tables listed earlier have precedence. We are also memoizing nil -- values, which can be overwritten if they are 's' (soft). --]] for _, t in ipairs(tables) do for key, val in pairs(t) do if metaArgs[key] == nil and nilArgs[key] ~= 'h' then local tidiedVal = tidyVal(key, val) if tidiedVal == nil then nilArgs[key] = 's' else metaArgs[key] = tidiedVal end end end end end --[[ -- Define metatable behaviour. Arguments are memoized in the metaArgs table, -- and are only fetched from the argument tables once. Fetching arguments -- from the argument tables is the most resource-intensive step in this -- module, so we try and avoid it where possible. For this reason, nil -- arguments are also memoized, in the nilArgs table. Also, we keep a record -- in the metatable of when pairs and ipairs have been called, so we do not -- run pairs and ipairs on the argument tables more than once. We also do -- not run ipairs on fargs and pargs if pairs has already been run, as all -- the arguments will already have been copied over. --]] metatable.__index = function (t, key) --[[ -- Fetches an argument when the args table is indexed. First we check -- to see if the value is memoized, and if not we try and fetch it from -- the argument tables. When we check memoization, we need to check -- metaArgs before nilArgs, as both can be non-nil at the same time. -- If the argument is not present in metaArgs, we also check whether -- pairs has been run yet. If pairs has already been run, we return nil. -- This is because all the arguments will have already been copied into -- metaArgs by the mergeArgs function, meaning that any other arguments -- must be nil. --]] if type(key) == 'string' then key = options.translate[key] end local val = metaArgs[key] if val ~= nil then return val elseif metatable.donePairs or nilArgs[key] then return nil end for _, argTable in ipairs(argTables) do local argTableVal = tidyVal(key, argTable[key]) if argTableVal ~= nil then metaArgs[key] = argTableVal return argTableVal end end nilArgs[key] = 'h' return nil end metatable.__newindex = function (t, key, val) -- This function is called when a module tries to add a new value to the -- args table, or tries to change an existing value. if type(key) == 'string' then key = options.translate[key] end if options.readOnly then error( 'could not write to argument table key "' .. tostring(key) .. '"; the table is read-only', 2 ) elseif options.noOverwrite and args[key] ~= nil then error( 'could not write to argument table key "' .. tostring(key) .. '"; overwriting existing arguments is not permitted', 2 ) elseif val == nil then --[[ -- If the argument is to be overwritten with nil, we need to erase -- the value in metaArgs, so that __index, __pairs and __ipairs do -- not use a previous existing value, if present; and we also need -- to memoize the nil in nilArgs, so that the value isn't looked -- up in the argument tables if it is accessed again. --]] metaArgs[key] = nil nilArgs[key] = 'h' else metaArgs[key] = val end end local function translatenext(invariant) local k, v = next(invariant.t, invariant.k) invariant.k = k if k == nil then return nil elseif type(k) ~= 'string' or not options.backtranslate then return k, v else local backtranslate = options.backtranslate[k] if backtranslate == nil then -- Skip this one. This is a tail call, so this won't cause stack overflow return translatenext(invariant) else return backtranslate, v end end end metatable.__pairs = function () -- Called when pairs is run on the args table. if not metatable.donePairs then mergeArgs(argTables) metatable.donePairs = true end return translatenext, { t = metaArgs } end local function inext(t, i) -- This uses our __index metamethod local v = t[i + 1] if v ~= nil then return i + 1, v end end metatable.__ipairs = function (t) -- Called when ipairs is run on the args table. return inext, t, 0 end return args end return arguments 3134ecce8429b810d445e29eae115e2ae4c36c53 Module:Documentation 828 20 33 2022-09-30T02:36:08Z dev>Pppery 0 Pppery moved page [[Module:Documentation/2]] to [[Module:Documentation]] without leaving a redirect Scribunto text/plain -- This module implements {{documentation}}. -- Get required modules. local getArgs = require('Module:Arguments').getArgs -- Get the config table. local cfg = mw.loadData('Module:Documentation/config') local p = {} -- Often-used functions. local ugsub = mw.ustring.gsub ---------------------------------------------------------------------------- -- Helper functions -- -- These are defined as local functions, but are made available in the p -- table for testing purposes. ---------------------------------------------------------------------------- local function message(cfgKey, valArray, expectType) --[[ -- Gets a message from the cfg table and formats it if appropriate. -- The function raises an error if the value from the cfg table is not -- of the type expectType. The default type for expectType is 'string'. -- If the table valArray is present, strings such as $1, $2 etc. in the -- message are substituted with values from the table keys [1], [2] etc. -- For example, if the message "foo-message" had the value 'Foo $2 bar $1.', -- message('foo-message', {'baz', 'qux'}) would return "Foo qux bar baz." --]] local msg = cfg[cfgKey] expectType = expectType or 'string' if type(msg) ~= expectType then error('message: type error in message cfg.' .. cfgKey .. ' (' .. expectType .. ' expected, got ' .. type(msg) .. ')', 2) end if not valArray then return msg end local function getMessageVal(match) match = tonumber(match) return valArray[match] or error('message: no value found for key $' .. match .. ' in message cfg.' .. cfgKey, 4) end return ugsub(msg, '$([1-9][0-9]*)', getMessageVal) end p.message = message local function makeWikilink(page, display) if display then return mw.ustring.format('[[%s|%s]]', page, display) else return mw.ustring.format('[[%s]]', page) end end p.makeWikilink = makeWikilink local function makeCategoryLink(cat, sort) local catns = mw.site.namespaces[14].name return makeWikilink(catns .. ':' .. cat, sort) end p.makeCategoryLink = makeCategoryLink local function makeUrlLink(url, display) return mw.ustring.format('[%s %s]', url, display) end p.makeUrlLink = makeUrlLink local function makeToolbar(...) local ret = {} local lim = select('#', ...) if lim < 1 then return nil end for i = 1, lim do ret[#ret + 1] = select(i, ...) end -- 'documentation-toolbar' return '<span class="' .. message('toolbar-class') .. '">(' .. table.concat(ret, ' &#124; ') .. ')</span>' end p.makeToolbar = makeToolbar ---------------------------------------------------------------------------- -- Argument processing ---------------------------------------------------------------------------- local function makeInvokeFunc(funcName) return function (frame) local args = getArgs(frame, { valueFunc = function (key, value) if type(value) == 'string' then value = value:match('^%s*(.-)%s*$') -- Remove whitespace. if key == 'heading' or value ~= '' then return value else return nil end else return value end end }) return p[funcName](args) end end ---------------------------------------------------------------------------- -- Entry points ---------------------------------------------------------------------------- function p.nonexistent(frame) if mw.title.getCurrentTitle().subpageText == 'testcases' then return frame:expandTemplate{title = 'module test cases notice'} else return p.main(frame) end end p.main = makeInvokeFunc('_main') function p._main(args) --[[ -- This function defines logic flow for the module. -- @args - table of arguments passed by the user --]] local env = p.getEnvironment(args) local root = mw.html.create() root :tag('div') -- 'documentation-container' :addClass(message('container')) :attr('role', 'complementary') :attr('aria-labelledby', args.heading ~= '' and 'documentation-heading' or nil) :attr('aria-label', args.heading == '' and 'Documentation' or nil) :newline() :tag('div') -- 'documentation' :addClass(message('main-div-classes')) :newline() :wikitext(p._startBox(args, env)) :wikitext(p._content(args, env)) :tag('div') -- 'documentation-clear' :addClass(message('clear')) :done() :newline() :done() :wikitext(p._endBox(args, env)) :done() :wikitext(p.addTrackingCategories(env)) -- 'Module:Documentation/styles.css' return mw.getCurrentFrame():extensionTag ( 'templatestyles', '', {src=cfg['templatestyles'] }) .. tostring(root) end ---------------------------------------------------------------------------- -- Environment settings ---------------------------------------------------------------------------- function p.getEnvironment(args) --[[ -- Returns a table with information about the environment, including title -- objects and other namespace- or path-related data. -- @args - table of arguments passed by the user -- -- Title objects include: -- env.title - the page we are making documentation for (usually the current title) -- env.templateTitle - the template (or module, file, etc.) -- env.docTitle - the /doc subpage. -- env.sandboxTitle - the /sandbox subpage. -- env.testcasesTitle - the /testcases subpage. -- -- Data includes: -- env.subjectSpace - the number of the title's subject namespace. -- env.docSpace - the number of the namespace the title puts its documentation in. -- env.docpageBase - the text of the base page of the /doc, /sandbox and /testcases pages, with namespace. -- env.compareUrl - URL of the Special:ComparePages page comparing the sandbox with the template. -- -- All table lookups are passed through pcall so that errors are caught. If an error occurs, the value -- returned will be nil. --]] local env, envFuncs = {}, {} -- Set up the metatable. If triggered we call the corresponding function in the envFuncs table. The value -- returned by that function is memoized in the env table so that we don't call any of the functions -- more than once. (Nils won't be memoized.) setmetatable(env, { __index = function (t, key) local envFunc = envFuncs[key] if envFunc then local success, val = pcall(envFunc) if success then env[key] = val -- Memoise the value. return val end end return nil end }) function envFuncs.title() -- The title object for the current page, or a test page passed with args.page. local title local titleArg = args.page if titleArg then title = mw.title.new(titleArg) else title = mw.title.getCurrentTitle() end return title end function envFuncs.templateTitle() --[[ -- The template (or module, etc.) title object. -- Messages: -- 'sandbox-subpage' --> 'sandbox' -- 'testcases-subpage' --> 'testcases' --]] local subjectSpace = env.subjectSpace local title = env.title local subpage = title.subpageText if subpage == message('sandbox-subpage') or subpage == message('testcases-subpage') then return mw.title.makeTitle(subjectSpace, title.baseText) else return mw.title.makeTitle(subjectSpace, title.text) end end function envFuncs.docTitle() --[[ -- Title object of the /doc subpage. -- Messages: -- 'doc-subpage' --> 'doc' --]] local title = env.title local docname = args[1] -- User-specified doc page. local docpage if docname then docpage = docname else docpage = env.docpageBase .. '/' .. message('doc-subpage') end return mw.title.new(docpage) end function envFuncs.sandboxTitle() --[[ -- Title object for the /sandbox subpage. -- Messages: -- 'sandbox-subpage' --> 'sandbox' --]] return mw.title.new(env.docpageBase .. '/' .. message('sandbox-subpage')) end function envFuncs.testcasesTitle() --[[ -- Title object for the /testcases subpage. -- Messages: -- 'testcases-subpage' --> 'testcases' --]] return mw.title.new(env.docpageBase .. '/' .. message('testcases-subpage')) end function envFuncs.subjectSpace() -- The subject namespace number. return mw.site.namespaces[env.title.namespace].subject.id end function envFuncs.docSpace() -- The documentation namespace number. For most namespaces this is the -- same as the subject namespace. However, pages in the Article, File, -- MediaWiki or Category namespaces must have their /doc, /sandbox and -- /testcases pages in talk space. local subjectSpace = env.subjectSpace if subjectSpace == 0 or subjectSpace == 6 or subjectSpace == 8 or subjectSpace == 14 then return subjectSpace + 1 else return subjectSpace end end function envFuncs.docpageBase() -- The base page of the /doc, /sandbox, and /testcases subpages. -- For some namespaces this is the talk page, rather than the template page. local templateTitle = env.templateTitle local docSpace = env.docSpace local docSpaceText = mw.site.namespaces[docSpace].name -- Assemble the link. docSpace is never the main namespace, so we can hardcode the colon. return docSpaceText .. ':' .. templateTitle.text end function envFuncs.compareUrl() -- Diff link between the sandbox and the main template using [[Special:ComparePages]]. local templateTitle = env.templateTitle local sandboxTitle = env.sandboxTitle if templateTitle.exists and sandboxTitle.exists then local compareUrl = mw.uri.fullUrl( 'Special:ComparePages', { page1 = templateTitle.prefixedText, page2 = sandboxTitle.prefixedText} ) return tostring(compareUrl) else return nil end end return env end ---------------------------------------------------------------------------- -- Start box ---------------------------------------------------------------------------- p.startBox = makeInvokeFunc('_startBox') function p._startBox(args, env) --[[ -- This function generates the start box. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- The actual work is done by p.makeStartBoxLinksData and p.renderStartBoxLinks which make -- the [view] [edit] [history] [purge] links, and by p.makeStartBoxData and p.renderStartBox -- which generate the box HTML. --]] env = env or p.getEnvironment(args) local links local content = args.content if not content or args[1] then -- No need to include the links if the documentation is on the template page itself. local linksData = p.makeStartBoxLinksData(args, env) if linksData then links = p.renderStartBoxLinks(linksData) end end -- Generate the start box html. local data = p.makeStartBoxData(args, env, links) if data then return p.renderStartBox(data) else -- User specified no heading. return nil end end function p.makeStartBoxLinksData(args, env) --[[ -- Does initial processing of data to make the [view] [edit] [history] [purge] links. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- Messages: -- 'view-link-display' --> 'view' -- 'edit-link-display' --> 'edit' -- 'history-link-display' --> 'history' -- 'purge-link-display' --> 'purge' -- 'module-preload' --> 'Template:Documentation/preload-module-doc' -- 'docpage-preload' --> 'Template:Documentation/preload' -- 'create-link-display' --> 'create' --]] local subjectSpace = env.subjectSpace local title = env.title local docTitle = env.docTitle if not title or not docTitle then return nil end if docTitle.isRedirect then docTitle = docTitle.redirectTarget end local data = {} data.title = title data.docTitle = docTitle -- View, display, edit, and purge links if /doc exists. data.viewLinkDisplay = message('view-link-display') data.editLinkDisplay = message('edit-link-display') data.historyLinkDisplay = message('history-link-display') data.purgeLinkDisplay = message('purge-link-display') -- Create link if /doc doesn't exist. local preload = args.preload if not preload then if subjectSpace == 828 then -- Module namespace preload = message('module-preload') else preload = message('docpage-preload') end end data.preload = preload data.createLinkDisplay = message('create-link-display') return data end function p.renderStartBoxLinks(data) --[[ -- Generates the [view][edit][history][purge] or [create][purge] links from the data table. -- @data - a table of data generated by p.makeStartBoxLinksData --]] local function escapeBrackets(s) -- Escapes square brackets with HTML entities. s = s:gsub('%[', '&#91;') -- Replace square brackets with HTML entities. s = s:gsub('%]', '&#93;') return s end local ret local docTitle = data.docTitle local title = data.title local purgeLink = makeUrlLink(title:fullUrl{action = 'purge'}, data.purgeLinkDisplay) if docTitle.exists then local viewLink = makeWikilink(docTitle.prefixedText, data.viewLinkDisplay) local editLink = makeUrlLink(docTitle:fullUrl{action = 'edit'}, data.editLinkDisplay) local historyLink = makeUrlLink(docTitle:fullUrl{action = 'history'}, data.historyLinkDisplay) ret = '[%s] [%s] [%s] [%s]' ret = escapeBrackets(ret) ret = mw.ustring.format(ret, viewLink, editLink, historyLink, purgeLink) else local createLink = makeUrlLink(docTitle:fullUrl{action = 'edit', preload = data.preload}, data.createLinkDisplay) ret = '[%s] [%s]' ret = escapeBrackets(ret) ret = mw.ustring.format(ret, createLink, purgeLink) end return ret end function p.makeStartBoxData(args, env, links) --[=[ -- Does initial processing of data to pass to the start-box render function, p.renderStartBox. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- @links - a string containing the [view][edit][history][purge] links - could be nil if there's an error. -- -- Messages: -- 'documentation-icon-wikitext' --> '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]' -- 'template-namespace-heading' --> 'Template documentation' -- 'module-namespace-heading' --> 'Module documentation' -- 'file-namespace-heading' --> 'Summary' -- 'other-namespaces-heading' --> 'Documentation' -- 'testcases-create-link-display' --> 'create' --]=] local subjectSpace = env.subjectSpace if not subjectSpace then -- Default to an "other namespaces" namespace, so that we get at least some output -- if an error occurs. subjectSpace = 2 end local data = {} -- Heading local heading = args.heading -- Blank values are not removed. if heading == '' then -- Don't display the start box if the heading arg is defined but blank. return nil end if heading then data.heading = heading elseif subjectSpace == 10 then -- Template namespace data.heading = message('documentation-icon-wikitext') .. ' ' .. message('template-namespace-heading') elseif subjectSpace == 828 then -- Module namespace data.heading = message('documentation-icon-wikitext') .. ' ' .. message('module-namespace-heading') elseif subjectSpace == 6 then -- File namespace data.heading = message('file-namespace-heading') else data.heading = message('other-namespaces-heading') end -- Heading CSS local headingStyle = args['heading-style'] if headingStyle then data.headingStyleText = headingStyle else -- 'documentation-heading' data.headingClass = message('main-div-heading-class') end -- Data for the [view][edit][history][purge] or [create] links. if links then -- 'mw-editsection-like plainlinks' data.linksClass = message('start-box-link-classes') data.links = links end return data end function p.renderStartBox(data) -- Renders the start box html. -- @data - a table of data generated by p.makeStartBoxData. local sbox = mw.html.create('div') sbox -- 'documentation-startbox' :addClass(message('start-box-class')) :newline() :tag('span') :addClass(data.headingClass) :attr('id', 'documentation-heading') :cssText(data.headingStyleText) :wikitext(data.heading) local links = data.links if links then sbox:tag('span') :addClass(data.linksClass) :attr('id', data.linksId) :wikitext(links) end return tostring(sbox) end ---------------------------------------------------------------------------- -- Documentation content ---------------------------------------------------------------------------- p.content = makeInvokeFunc('_content') function p._content(args, env) -- Displays the documentation contents -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment env = env or p.getEnvironment(args) local docTitle = env.docTitle local content = args.content if not content and docTitle and docTitle.exists then content = args._content or mw.getCurrentFrame():expandTemplate{title = docTitle.prefixedText} end -- The line breaks below are necessary so that "=== Headings ===" at the start and end -- of docs are interpreted correctly. return '\n' .. (content or '') .. '\n' end p.contentTitle = makeInvokeFunc('_contentTitle') function p._contentTitle(args, env) env = env or p.getEnvironment(args) local docTitle = env.docTitle if not args.content and docTitle and docTitle.exists then return docTitle.prefixedText else return '' end end ---------------------------------------------------------------------------- -- End box ---------------------------------------------------------------------------- p.endBox = makeInvokeFunc('_endBox') function p._endBox(args, env) --[=[ -- This function generates the end box (also known as the link box). -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- --]=] -- Get environment data. env = env or p.getEnvironment(args) local subjectSpace = env.subjectSpace local docTitle = env.docTitle if not subjectSpace or not docTitle then return nil end -- Check whether we should output the end box at all. Add the end -- box by default if the documentation exists or if we are in the -- user, module or template namespaces. local linkBox = args['link box'] if linkBox == 'off' or not ( docTitle.exists or subjectSpace == 2 or subjectSpace == 828 or subjectSpace == 10 ) then return nil end -- Assemble the link box. local text = '' if linkBox then text = text .. linkBox else text = text .. (p.makeDocPageBlurb(args, env) or '') -- "This documentation is transcluded from [[Foo]]." if subjectSpace == 2 or subjectSpace == 10 or subjectSpace == 828 then -- We are in the user, template or module namespaces. -- Add sandbox and testcases links. -- "Editors can experiment in this template's sandbox and testcases pages." text = text .. (p.makeExperimentBlurb(args, env) or '') .. '<br />' if not args.content and not args[1] then -- "Please add categories to the /doc subpage." -- Don't show this message with inline docs or with an explicitly specified doc page, -- as then it is unclear where to add the categories. text = text .. (p.makeCategoriesBlurb(args, env) or '') end text = text .. ' ' .. (p.makeSubpagesBlurb(args, env) or '') --"Subpages of this template" end end local box = mw.html.create('div') -- 'documentation-metadata' box:attr('role', 'note') :addClass(message('end-box-class')) -- 'plainlinks' :addClass(message('end-box-plainlinks')) :wikitext(text) :done() return '\n' .. tostring(box) end function p.makeDocPageBlurb(args, env) --[=[ -- Makes the blurb "This documentation is transcluded from [[Template:Foo]] (edit, history)". -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- Messages: -- 'edit-link-display' --> 'edit' -- 'history-link-display' --> 'history' -- 'transcluded-from-blurb' --> -- 'The above [[Wikipedia:Template documentation|documentation]] -- is [[Help:Transclusion|transcluded]] from $1.' -- 'module-preload' --> 'Template:Documentation/preload-module-doc' -- 'create-link-display' --> 'create' -- 'create-module-doc-blurb' --> -- 'You might want to $1 a documentation page for this [[Wikipedia:Lua|Scribunto module]].' --]=] local docTitle = env.docTitle if not docTitle then return nil end local ret if docTitle.exists then -- /doc exists; link to it. local docLink = makeWikilink(docTitle.prefixedText) local editUrl = docTitle:fullUrl{action = 'edit'} local editDisplay = message('edit-link-display') local editLink = makeUrlLink(editUrl, editDisplay) local historyUrl = docTitle:fullUrl{action = 'history'} local historyDisplay = message('history-link-display') local historyLink = makeUrlLink(historyUrl, historyDisplay) ret = message('transcluded-from-blurb', {docLink}) .. ' ' .. makeToolbar(editLink, historyLink) .. '<br />' elseif env.subjectSpace == 828 then -- /doc does not exist; ask to create it. local createUrl = docTitle:fullUrl{action = 'edit', preload = message('module-preload')} local createDisplay = message('create-link-display') local createLink = makeUrlLink(createUrl, createDisplay) ret = message('create-module-doc-blurb', {createLink}) .. '<br />' end return ret end function p.makeExperimentBlurb(args, env) --[[ -- Renders the text "Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages." -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- Messages: -- 'sandbox-link-display' --> 'sandbox' -- 'sandbox-edit-link-display' --> 'edit' -- 'compare-link-display' --> 'diff' -- 'module-sandbox-preload' --> 'Template:Documentation/preload-module-sandbox' -- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox' -- 'sandbox-create-link-display' --> 'create' -- 'mirror-edit-summary' --> 'Create sandbox version of $1' -- 'mirror-link-display' --> 'mirror' -- 'mirror-link-preload' --> 'Template:Documentation/mirror' -- 'sandbox-link-display' --> 'sandbox' -- 'testcases-link-display' --> 'testcases' -- 'testcases-edit-link-display'--> 'edit' -- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox' -- 'testcases-create-link-display' --> 'create' -- 'testcases-link-display' --> 'testcases' -- 'testcases-edit-link-display' --> 'edit' -- 'module-testcases-preload' --> 'Template:Documentation/preload-module-testcases' -- 'template-testcases-preload' --> 'Template:Documentation/preload-testcases' -- 'experiment-blurb-module' --> 'Editors can experiment in this module's $1 and $2 pages.' -- 'experiment-blurb-template' --> 'Editors can experiment in this template's $1 and $2 pages.' --]] local subjectSpace = env.subjectSpace local templateTitle = env.templateTitle local sandboxTitle = env.sandboxTitle local testcasesTitle = env.testcasesTitle local templatePage = templateTitle.prefixedText if not subjectSpace or not templateTitle or not sandboxTitle or not testcasesTitle then return nil end -- Make links. local sandboxLinks, testcasesLinks if sandboxTitle.exists then local sandboxPage = sandboxTitle.prefixedText local sandboxDisplay = message('sandbox-link-display') local sandboxLink = makeWikilink(sandboxPage, sandboxDisplay) local sandboxEditUrl = sandboxTitle:fullUrl{action = 'edit'} local sandboxEditDisplay = message('sandbox-edit-link-display') local sandboxEditLink = makeUrlLink(sandboxEditUrl, sandboxEditDisplay) local compareUrl = env.compareUrl local compareLink if compareUrl then local compareDisplay = message('compare-link-display') compareLink = makeUrlLink(compareUrl, compareDisplay) end sandboxLinks = sandboxLink .. ' ' .. makeToolbar(sandboxEditLink, compareLink) else local sandboxPreload if subjectSpace == 828 then sandboxPreload = message('module-sandbox-preload') else sandboxPreload = message('template-sandbox-preload') end local sandboxCreateUrl = sandboxTitle:fullUrl{action = 'edit', preload = sandboxPreload} local sandboxCreateDisplay = message('sandbox-create-link-display') local sandboxCreateLink = makeUrlLink(sandboxCreateUrl, sandboxCreateDisplay) local mirrorSummary = message('mirror-edit-summary', {makeWikilink(templatePage)}) local mirrorPreload = message('mirror-link-preload') local mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = mirrorPreload, summary = mirrorSummary} if subjectSpace == 828 then mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = templateTitle.prefixedText, summary = mirrorSummary} end local mirrorDisplay = message('mirror-link-display') local mirrorLink = makeUrlLink(mirrorUrl, mirrorDisplay) sandboxLinks = message('sandbox-link-display') .. ' ' .. makeToolbar(sandboxCreateLink, mirrorLink) end if testcasesTitle.exists then local testcasesPage = testcasesTitle.prefixedText local testcasesDisplay = message('testcases-link-display') local testcasesLink = makeWikilink(testcasesPage, testcasesDisplay) local testcasesEditUrl = testcasesTitle:fullUrl{action = 'edit'} local testcasesEditDisplay = message('testcases-edit-link-display') local testcasesEditLink = makeUrlLink(testcasesEditUrl, testcasesEditDisplay) -- for Modules, add testcases run link if exists if testcasesTitle.contentModel == "Scribunto" and testcasesTitle.talkPageTitle and testcasesTitle.talkPageTitle.exists then local testcasesRunLinkDisplay = message('testcases-run-link-display') local testcasesRunLink = makeWikilink(testcasesTitle.talkPageTitle.prefixedText, testcasesRunLinkDisplay) testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink, testcasesRunLink) else testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink) end else local testcasesPreload if subjectSpace == 828 then testcasesPreload = message('module-testcases-preload') else testcasesPreload = message('template-testcases-preload') end local testcasesCreateUrl = testcasesTitle:fullUrl{action = 'edit', preload = testcasesPreload} local testcasesCreateDisplay = message('testcases-create-link-display') local testcasesCreateLink = makeUrlLink(testcasesCreateUrl, testcasesCreateDisplay) testcasesLinks = message('testcases-link-display') .. ' ' .. makeToolbar(testcasesCreateLink) end local messageName if subjectSpace == 828 then messageName = 'experiment-blurb-module' else messageName = 'experiment-blurb-template' end return message(messageName, {sandboxLinks, testcasesLinks}) end function p.makeCategoriesBlurb(args, env) --[[ -- Generates the text "Please add categories to the /doc subpage." -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- Messages: -- 'doc-link-display' --> '/doc' -- 'add-categories-blurb' --> 'Please add categories to the $1 subpage.' --]] local docTitle = env.docTitle if not docTitle then return nil end local docPathLink = makeWikilink(docTitle.prefixedText, message('doc-link-display')) return message('add-categories-blurb', {docPathLink}) end function p.makeSubpagesBlurb(args, env) --[[ -- Generates the "Subpages of this template" link. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- Messages: -- 'template-pagetype' --> 'template' -- 'module-pagetype' --> 'module' -- 'default-pagetype' --> 'page' -- 'subpages-link-display' --> 'Subpages of this $1' --]] local subjectSpace = env.subjectSpace local templateTitle = env.templateTitle if not subjectSpace or not templateTitle then return nil end local pagetype if subjectSpace == 10 then pagetype = message('template-pagetype') elseif subjectSpace == 828 then pagetype = message('module-pagetype') else pagetype = message('default-pagetype') end local subpagesLink = makeWikilink( 'Special:PrefixIndex/' .. templateTitle.prefixedText .. '/', message('subpages-link-display', {pagetype}) ) return message('subpages-blurb', {subpagesLink}) end ---------------------------------------------------------------------------- -- Tracking categories ---------------------------------------------------------------------------- function p.addTrackingCategories(env) --[[ -- Check if {{documentation}} is transcluded on a /doc or /testcases page. -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- Messages: -- 'display-strange-usage-category' --> true -- 'doc-subpage' --> 'doc' -- 'testcases-subpage' --> 'testcases' -- 'strange-usage-category' --> 'Wikipedia pages with strange ((documentation)) usage' -- -- /testcases pages in the module namespace are not categorised, as they may have -- {{documentation}} transcluded automatically. --]] local title = env.title local subjectSpace = env.subjectSpace if not title or not subjectSpace then return nil end local subpage = title.subpageText local ret = '' if message('display-strange-usage-category', nil, 'boolean') and ( subpage == message('doc-subpage') or subjectSpace ~= 828 and subpage == message('testcases-subpage') ) then ret = ret .. makeCategoryLink(message('strange-usage-category')) end return ret end return p 78cc3a78f2b5dbb267fa16027c0800a22dbd3c42 Module:Userbox 828 94 180 2022-09-30T13:34:01Z dev>Pppery 0 Let's not use [[Module:Category handler]] and its dependency web here please Scribunto text/plain -- This module implements {{userbox}}. local p = {} -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function checkNum(val, default) -- Checks whether a value is a number greater than or equal to zero. If so, -- returns it as a number. If not, returns a default value. val = tonumber(val) if val and val >= 0 then return val else return default end end local function addSuffix(num, suffix) -- Turns a number into a string and adds a suffix. if num then return tostring(num) .. suffix else return nil end end local function checkNumAndAddSuffix(num, default, suffix) -- Checks a value with checkNum and adds a suffix. num = checkNum(num, default) return addSuffix(num, suffix) end local function makeCat(cat, sort) -- Makes a category link. if sort then return mw.ustring.format('[[Category:%s|%s]]', cat, sort) else return mw.ustring.format('[[Category:%s]]', cat) end end -------------------------------------------------------------------------------- -- Argument processing -------------------------------------------------------------------------------- local function makeInvokeFunc(funcName) return function (frame) local origArgs = require('Module:Arguments').getArgs(frame) local args = {} for k, v in pairs(origArgs) do args[k] = v end return p.main(funcName, args) end end p.userbox = makeInvokeFunc('_userbox') p['userbox-2'] = makeInvokeFunc('_userbox-2') p['userbox-r'] = makeInvokeFunc('_userbox-r') -------------------------------------------------------------------------------- -- Main functions -------------------------------------------------------------------------------- function p.main(funcName, args) local userboxData = p[funcName](args) local userbox = p.render(userboxData) local cats = p.categories(args) return userbox .. (cats or '') end function p._userbox(args) -- Does argument processing for {{userbox}}. local data = {} -- Get div tag values. data.float = args.float or 'left' local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width. data.borderWidth = addSuffix(borderWidthNum, 'px') data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999' data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag. data.bodyClass = args.bodyclass -- Get table tag values. data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee' -- Get info values. data.info = args.info or args[4] or "<code>{{{info}}}</code>" data.infoTextAlign = args['info-a'] or 'left' data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt') data.infoHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px') data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px' data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em' data.infoColor = args['info-color'] or args['info-fc'] or 'black' data.infoOtherParams = args['info-other-param'] or args['info-op'] data.infoClass = args['info-class'] -- Get id values. local id = args.logo or args[3] or args.id data.id = id data.showId = id and true or false data.idWidth = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px') data.idHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px') data.idBackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd' data.idTextAlign = args['id-a'] or 'center' data.idFontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt') data.idColor = args['logo-color'] or args['id-fc'] or data.infoColor data.idPadding = args['logo-padding'] or args['id-p'] or '0 1px 0 0' data.idLineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em' data.idOtherParams = args['logo-other-param'] or args['id-op'] data.idClass = args['id-class'] return data end p['_userbox-2'] = function (args) -- Does argument processing for {{userbox-2}}. local data = {} -- Get div tag values. data.float = args.float or 'left' local borderWidthNum = checkNum(args[9] or args['border-s'], 1) -- Used to calculate width. data.borderWidth = addSuffix(borderWidthNum, 'px') data.borderColor = args[1] or args['border-c'] or args['id1-c'] or '#999999' data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag. data.bodyClass = args.bodyclass -- Get table tag values. data.backgroundColor = args[2] or args['info-c'] or '#eeeeee' -- Get info values. data.info = args[4] or args.info or "<code>{{{info}}}</code>" data.infoTextAlign = args['info-a'] or 'left' data.infoFontSize = checkNumAndAddSuffix(args['info-s'], 8, 'pt') data.infoColor = args[8] or args['info-fc'] or 'black' data.infoPadding = args['info-p'] or '0 4px 0 4px' data.infoLineHeight = args['info-lh'] or '1.25em' data.infoOtherParams = args['info-op'] -- Get id values. data.showId = true data.id = args.logo or args[3] or args.id1 or 'id1' data.idWidth = checkNumAndAddSuffix(args['id1-w'], 45, 'px') data.idHeight = checkNumAndAddSuffix(args['id-h'], 45, 'px') data.idBackgroundColor = args[1] or args['id1-c'] or '#dddddd' data.idTextAlign = 'center' data.idFontSize = checkNumAndAddSuffix(args['id1-s'], 14, 'pt') data.idLineHeight = args['id1-lh'] or '1.25em' data.idColor = args['id1-fc'] or data.infoColor data.idPadding = args['id1-p'] or '0 1px 0 0' data.idOtherParams = args['id1-op'] -- Get id2 values. data.showId2 = true data.id2 = args.logo or args[5] or args.id2 or 'id2' data.id2Width = checkNumAndAddSuffix(args['id2-w'], 45, 'px') data.id2Height = data.idHeight data.id2BackgroundColor = args[7] or args['id2-c'] or args[1] or '#dddddd' data.id2TextAlign = 'center' data.id2FontSize = checkNumAndAddSuffix(args['id2-s'], 14, 'pt') data.id2LineHeight = args['id2-lh'] or '1.25em' data.id2Color = args['id2-fc'] or data.infoColor data.id2Padding = args['id2-p'] or '0 0 0 1px' data.id2OtherParams = args['id2-op'] return data end p['_userbox-r'] = function (args) -- Does argument processing for {{userbox-r}}. local data = {} -- Get div tag values. data.float = args.float or 'left' local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width. data.borderWidth = addSuffix(borderWidthNum, 'px') data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999' data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag. data.bodyClass = args.bodyclass -- Get table tag values. data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee' -- Get id values. data.showId = false -- We only show id2 in userbox-r. -- Get info values. data.info = args.info or args[4] or "<code>{{{info}}}</code>" data.infoTextAlign = args['info-align'] or args['info-a'] or 'left' data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt') data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px' data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em' data.infoColor = args['info-color'] or args['info-fc'] or 'black' data.infoOtherParams = args['info-other-param'] or args['info-op'] -- Get id2 values. data.showId2 = true data.id2 = args.logo or args[3] or args.id or 'id' data.id2Width = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px') data.id2Height = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px') data.id2BackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd' data.id2TextAlign = args['id-a'] or 'center' data.id2FontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt') data.id2Color = args['logo-color'] or args['id-fc'] or data.infoColor data.id2Padding = args['logo-padding'] or args['id-p'] or '0 0 0 1px' data.id2LineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em' data.id2OtherParams = args['logo-other-param'] or args['id-op'] return data end function p.render(data) -- Renders the userbox html using the content of the data table. -- Render the div tag html. local root = mw.html.create('div') root :css('float', data.float) :css('border', (data.borderWidth or '') .. ' solid ' .. (data.borderColor or '')) :css('margin', '1px') :css('width', data.width) :addClass('wikipediauserbox') :addClass(data.bodyClass) -- Render the table tag html. local tableroot = root:tag('table') tableroot :css('border-collapse', 'collapse') :css('width', data.width) :css('margin-bottom', '0') :css('margin-top', '0') :css('background', data.backgroundColor) -- Render the id html. local tablerow = tableroot:tag('tr') if data.showId then tablerow:tag('th') :css('border', '0') :css('width', data.idWidth) :css('height', data.idHeight) :css('background', data.idBackgroundColor) :css('text-align', data.idTextAlign) :css('font-size', data.idFontSize) :css('color', data.idColor) :css('padding', data.idPadding) :css('line-height', data.idLineHeight) :css('vertical-align', 'middle') :cssText(data.idOtherParams) :addClass(data.idClass) :wikitext(data.id) end -- Render the info html. tablerow:tag('td') :css('border', '0') :css('text-align', data.infoTextAlign) :css('font-size', data.infoFontSize) :css('padding', data.infoPadding) :css('height', data.infoHeight) :css('line-height', data.infoLineHeight) :css('color', data.infoColor) :css('vertical-align', 'middle') :cssText(data.infoOtherParams) :addClass(data.infoClass) :wikitext(data.info) -- Render the second id html. if data.showId2 then tablerow:tag('th') :css('border', '0') :css('width', data.id2Width) :css('height', data.id2Height) :css('background', data.id2BackgroundColor) :css('text-align', data.id2TextAlign) :css('font-size', data.id2FontSize) :css('color', data.id2Color) :css('padding', data.id2Padding) :css('line-height', data.id2LineHeight) :css('vertical-align', 'middle') :cssText(data.id2OtherParams) :wikitext(data.id2) end local title = mw.title.getCurrentTitle() if (title.namespace == 2) and not title.text:match("/") then return tostring(root) -- regular user page elseif title.namespace == 14 then return tostring(root) -- category elseif title.isTalkPage then return tostring(root) -- talk page end local function has_text(wikitext) local function get_alt(text) return text:match("|alt=([^|]*)") or "" end wikitext = wikitext:gsub("]]", "|]]") wikitext = wikitext:gsub("%[%[%s*[Mm][Ee][Dd][Ii][Aa]%s*:[^|]-(|.-)]]", get_alt) wikitext = wikitext:gsub("%[%[%s*[Ii][Mm][Aa][Gg][Ee]%s*:[^|]-(|.-)]]", get_alt) wikitext = wikitext:gsub("%[%[%s*[Ff][Ii][Ll][Ee]%s*:[^|]-(|.-)]]", get_alt) return mw.text.trim(wikitext) ~= "" end return tostring(root) end function p.categories(args, page) -- Gets categories -- The page parameter makes the function act as though the module was being called from that page. -- It is included for testing purposes. local cats = {} cats[#cats + 1] = args.usercategory cats[#cats + 1] = args.usercategory2 cats[#cats + 1] = args.usercategory3 if #cats > 0 and not require("Module:Yesno")(args.nocat) then -- Get the title object local title if page then title = mw.title.new(page) else title = mw.title.getCurrentTitle() end -- Build category handler arguments. local chargs = {} chargs.page = page chargs.main = '[[Category:Pages with misplaced templates]]' if title.namespace == 2 then -- User namespace. local user = '' for i, cat in ipairs(cats) do user = user .. makeCat(cat) end return user elseif title.namespace == 10 then -- Template namespace. local basepage = title.baseText local template = '' for i, cat in ipairs(cats) do template = template .. makeCat(cat, ' ' .. basepage) end return template end end end return p aac333efff739f0243d8ffced6f4296cffb8d7e9 Template:Infobox 10 17 27 2022-09-30T14:45:57Z dev>Pppery 0 Copy from Wikipedia wikitext text/x-wiki {{#invoke:Infobox|infobox}}<noinclude> {{documentation}} </noinclude> 627ee6fcf4d4f108fe054b5c476201cad0ed7717 Module:Infobox 828 19 31 2022-09-30T14:52:23Z dev>Pppery 0 Scribunto text/plain -- -- This module implements {{Infobox}} -- local p = {} local args = {} local origArgs = {} local root local function notempty( s ) return s and s:match( '%S' ) end local function fixChildBoxes(sval, tt) if notempty(sval) then local marker = '<span class=special_infobox_marker>' local s = sval s = mw.ustring.gsub(s, '(<%s*[Tt][Rr])', marker .. '%1') s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>)', '%1' .. marker) if s:match(marker) then s = mw.ustring.gsub(s, marker .. '%s*' .. marker, '') s = mw.ustring.gsub(s, '([\r\n]|-[^\r\n]*[\r\n])%s*' .. marker, '%1') s = mw.ustring.gsub(s, marker .. '%s*([\r\n]|-)', '%1') s = mw.ustring.gsub(s, '(</[Cc][Aa][Pp][Tt][Ii][Oo][Nn]%s*>%s*)' .. marker, '%1') s = mw.ustring.gsub(s, '(<%s*[Tt][Aa][Bb][Ll][Ee][^<>]*>%s*)' .. marker, '%1') s = mw.ustring.gsub(s, '^(%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1') s = mw.ustring.gsub(s, '([\r\n]%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1') s = mw.ustring.gsub(s, marker .. '(%s*</[Tt][Aa][Bb][Ll][Ee]%s*>)', '%1') s = mw.ustring.gsub(s, marker .. '(%s*\n|%})', '%1') end if s:match(marker) then local subcells = mw.text.split(s, marker) s = '' for k = 1, #subcells do if k == 1 then s = s .. subcells[k] .. '</' .. tt .. '></tr>' elseif k == #subcells then local rowstyle = ' style="display:none"' if notempty(subcells[k]) then rowstyle = '' end s = s .. '<tr' .. rowstyle ..'><' .. tt .. ' colspan=2>\n' .. subcells[k] elseif notempty(subcells[k]) then if (k % 2) == 0 then s = s .. subcells[k] else s = s .. '<tr><' .. tt .. ' colspan=2>\n' .. subcells[k] .. '</' .. tt .. '></tr>' end end end end -- the next two lines add a newline at the end of lists for the PHP parser -- https://en.wikipedia.org/w/index.php?title=Template_talk:Infobox_musical_artist&oldid=849054481 -- remove when [[:phab:T191516]] is fixed or OBE s = mw.ustring.gsub(s, '([\r\n][%*#;:][^\r\n]*)$', '%1\n') s = mw.ustring.gsub(s, '^([%*#;:][^\r\n]*)$', '%1\n') s = mw.ustring.gsub(s, '^([%*#;:])', '\n%1') s = mw.ustring.gsub(s, '^(%{%|)', '\n%1') return s else return sval end end local function union(t1, t2) -- Returns the union of the values of two tables, as a sequence. local vals = {} for k, v in pairs(t1) do vals[v] = true end for k, v in pairs(t2) do vals[v] = true end local ret = {} for k, v in pairs(vals) do table.insert(ret, k) end return ret end local function getArgNums(prefix) -- Returns a table containing the numbers of the arguments that exist -- for the specified prefix. For example, if the prefix was 'data', and -- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}. local nums = {} for k, v in pairs(args) do local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$') if num then table.insert(nums, tonumber(num)) end end table.sort(nums) return nums end local function addRow(rowArgs) -- Adds a row to the infobox, with either a header cell -- or a label/data cell combination. if rowArgs.header and rowArgs.header ~= '_BLANK_' then root :tag('tr') :addClass(rowArgs.rowclass) :cssText(rowArgs.rowstyle) :attr('id', rowArgs.rowid) :tag('th') :attr('colspan', 2) :attr('id', rowArgs.headerid) :addClass(rowArgs.class) :addClass(args.headerclass) :css('text-align', 'center') :cssText(args.headerstyle) :cssText(rowArgs.rowcellstyle) :wikitext(fixChildBoxes(rowArgs.header, 'th')) elseif rowArgs.data then if not rowArgs.data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then rowArgs.rowstyle = 'display:none' end local row = root:tag('tr') row:addClass(rowArgs.rowclass) row:cssText(rowArgs.rowstyle) row:attr('id', rowArgs.rowid) if rowArgs.label then row :tag('th') :attr('scope', 'row') :attr('id', rowArgs.labelid) :cssText(args.labelstyle) :cssText(rowArgs.rowcellstyle) :wikitext(rowArgs.label) :done() end local dataCell = row:tag('td') if not rowArgs.label then dataCell :attr('colspan', 2) :css('text-align', 'center') end dataCell :attr('id', rowArgs.dataid) :addClass(rowArgs.class) :cssText(rowArgs.datastyle) :cssText(rowArgs.rowcellstyle) :wikitext(fixChildBoxes(rowArgs.data, 'td')) end end local function renderTitle() if not args.title then return end root :tag('caption') :addClass(args.titleclass) :cssText(args.titlestyle) :wikitext(args.title) end local function renderAboveRow() if not args.above then return end root :tag('tr') :tag('th') :attr('colspan', 2) :addClass(args.aboveclass) :css('text-align', 'center') :css('font-size', '125%') :css('font-weight', 'bold') :cssText(args.abovestyle) :wikitext(fixChildBoxes(args.above,'th')) end local function renderBelowRow() if not args.below then return end root :tag('tr') :tag('td') :attr('colspan', '2') :addClass(args.belowclass) :css('text-align', 'center') :cssText(args.belowstyle) :wikitext(fixChildBoxes(args.below,'td')) end local function renderSubheaders() if args.subheader then args.subheader1 = args.subheader end if args.subheaderrowclass then args.subheaderrowclass1 = args.subheaderrowclass end local subheadernums = getArgNums('subheader') for k, num in ipairs(subheadernums) do addRow({ data = args['subheader' .. tostring(num)], datastyle = args.subheaderstyle, rowcellstyle = args['subheaderstyle' .. tostring(num)], class = args.subheaderclass, rowclass = args['subheaderrowclass' .. tostring(num)] }) end end local function renderImages() if args.image then args.image1 = args.image end if args.caption then args.caption1 = args.caption end local imagenums = getArgNums('image') for k, num in ipairs(imagenums) do local caption = args['caption' .. tostring(num)] local data = mw.html.create():wikitext(args['image' .. tostring(num)]) if caption then data :tag('div') :cssText(args.captionstyle) :wikitext(caption) end addRow({ data = tostring(data), datastyle = args.imagestyle, class = args.imageclass, rowclass = args['imagerowclass' .. tostring(num)] }) end end local function preprocessRows() -- Gets the union of the header and data argument numbers, -- and renders them all in order using addRow. local rownums = union(getArgNums('header'), getArgNums('data')) table.sort(rownums) local lastheader for k, num in ipairs(rownums) do if args['header' .. tostring(num)] then if lastheader then args['header' .. tostring(lastheader)] = nil end lastheader = num elseif args['data' .. tostring(num)] and args['data' .. tostring(num)]:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then local data = args['data' .. tostring(num)] if data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('%S') then lastheader = nil end end end if lastheader then args['header' .. tostring(lastheader)] = nil end end local function renderRows() -- Gets the union of the header and data argument numbers, -- and renders them all in order using addRow. local rownums = union(getArgNums('header'), getArgNums('data')) table.sort(rownums) for k, num in ipairs(rownums) do addRow({ header = args['header' .. tostring(num)], label = args['label' .. tostring(num)], data = args['data' .. tostring(num)], datastyle = args.datastyle, class = args['class' .. tostring(num)], rowclass = args['rowclass' .. tostring(num)], rowstyle = args['rowstyle' .. tostring(num)], rowcellstyle = args['rowcellstyle' .. tostring(num)], dataid = args['dataid' .. tostring(num)], labelid = args['labelid' .. tostring(num)], headerid = args['headerid' .. tostring(num)], rowid = args['rowid' .. tostring(num)] }) end end local function renderItalicTitle() local italicTitle = args['italic title'] and mw.ustring.lower(args['italic title']) if italicTitle == '' or italicTitle == 'force' or italicTitle == 'yes' then root:wikitext(mw.getCurrentFrame():expandTemplate({title = 'italic title'})) end end local function _infobox() -- Specify the overall layout of the infobox, with special settings -- if the infobox is used as a 'child' inside another infobox. if args.child ~= 'yes' then root = mw.html.create('table') root :addClass((args.subbox ~= 'yes') and 'infobox' or nil) :addClass(args.bodyclass) if args.subbox == 'yes' then root :css('padding', '0') :css('border', 'none') :css('margin', '-3px') :css('width', 'auto') :css('min-width', '100%') :css('font-size', '100%') :css('clear', 'none') :css('float', 'none') :css('background-color', 'transparent') else root :css('width', '22em') end root :cssText(args.bodystyle) renderTitle() renderAboveRow() else root = mw.html.create() root :wikitext(args.title) end renderSubheaders() renderImages() if args.autoheaders then preprocessRows() end renderRows() renderBelowRow() renderItalicTitle() return tostring(root) end local function preprocessSingleArg(argName) -- If the argument exists and isn't blank, add it to the argument table. -- Blank arguments are treated as nil to match the behaviour of ParserFunctions. if origArgs[argName] and origArgs[argName] ~= '' then args[argName] = origArgs[argName] end end local function preprocessArgs(prefixTable, step) -- Assign the parameters with the given prefixes to the args table, in order, in batches -- of the step size specified. This is to prevent references etc. from appearing in the -- wrong order. The prefixTable should be an array containing tables, each of which has -- two possible fields, a "prefix" string and a "depend" table. The function always parses -- parameters containing the "prefix" string, but only parses parameters in the "depend" -- table if the prefix parameter is present and non-blank. if type(prefixTable) ~= 'table' then error("Non-table value detected for the prefix table", 2) end if type(step) ~= 'number' then error("Invalid step value detected", 2) end -- Get arguments without a number suffix, and check for bad input. for i,v in ipairs(prefixTable) do if type(v) ~= 'table' or type(v.prefix) ~= "string" or (v.depend and type(v.depend) ~= 'table') then error('Invalid input detected to preprocessArgs prefix table', 2) end preprocessSingleArg(v.prefix) -- Only parse the depend parameter if the prefix parameter is present and not blank. if args[v.prefix] and v.depend then for j, dependValue in ipairs(v.depend) do if type(dependValue) ~= 'string' then error('Invalid "depend" parameter value detected in preprocessArgs') end preprocessSingleArg(dependValue) end end end -- Get arguments with number suffixes. local a = 1 -- Counter variable. local moreArgumentsExist = true while moreArgumentsExist == true do moreArgumentsExist = false for i = a, a + step - 1 do for j,v in ipairs(prefixTable) do local prefixArgName = v.prefix .. tostring(i) if origArgs[prefixArgName] then moreArgumentsExist = true -- Do another loop if any arguments are found, even blank ones. preprocessSingleArg(prefixArgName) end -- Process the depend table if the prefix argument is present and not blank, or -- we are processing "prefix1" and "prefix" is present and not blank, and -- if the depend table is present. if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then for j,dependValue in ipairs(v.depend) do local dependArgName = dependValue .. tostring(i) preprocessSingleArg(dependArgName) end end end end a = a + step end end local function parseDataParameters() -- Parse the data parameters in the same order that the old {{infobox}} did, so that -- references etc. will display in the expected places. Parameters that depend on -- another parameter are only processed if that parameter is present, to avoid -- phantom references appearing in article reference lists. preprocessSingleArg('autoheaders') preprocessSingleArg('child') preprocessSingleArg('bodyclass') preprocessSingleArg('subbox') preprocessSingleArg('bodystyle') preprocessSingleArg('title') preprocessSingleArg('titleclass') preprocessSingleArg('titlestyle') preprocessSingleArg('above') preprocessSingleArg('aboveclass') preprocessSingleArg('abovestyle') preprocessArgs({ {prefix = 'subheader', depend = {'subheaderstyle', 'subheaderrowclass'}} }, 10) preprocessSingleArg('subheaderstyle') preprocessSingleArg('subheaderclass') preprocessArgs({ {prefix = 'image', depend = {'caption', 'imagerowclass'}} }, 10) preprocessSingleArg('captionstyle') preprocessSingleArg('imagestyle') preprocessSingleArg('imageclass') preprocessArgs({ {prefix = 'header'}, {prefix = 'data', depend = {'label'}}, {prefix = 'rowclass'}, {prefix = 'rowstyle'}, {prefix = 'rowcellstyle'}, {prefix = 'class'}, {prefix = 'dataid'}, {prefix = 'labelid'}, {prefix = 'headerid'}, {prefix = 'rowid'} }, 50) preprocessSingleArg('headerclass') preprocessSingleArg('headerstyle') preprocessSingleArg('labelstyle') preprocessSingleArg('datastyle') preprocessSingleArg('below') preprocessSingleArg('belowclass') preprocessSingleArg('belowstyle') preprocessSingleArg('name') args['italic title'] = origArgs['italic title'] -- different behaviour if blank or absent preprocessSingleArg('decat') end function p.infobox(frame) -- If called via #invoke, use the args passed into the invoking template. -- Otherwise, for testing purposes, assume args are being passed directly in. if frame == mw.getCurrentFrame() then origArgs = frame:getParent().args else origArgs = frame end parseDataParameters() return _infobox() end function p.infoboxTemplate(frame) -- For calling via #invoke within a template origArgs = {} for k,v in pairs(frame.args) do origArgs[k] = mw.text.trim(v) end parseDataParameters() return _infobox() end return p c6ac51f9e2faf9c2f3aba1fb8c05af98db47f4d4 Template:Utc 10 79 150 2022-09-30T16:16:46Z dev>Pppery 0 36 revisions imported from [[:wikipedia:Template:Utc]] wikitext text/x-wiki {{#time:H:i|{{#expr:{{{1|0}}} * 60 + {{{2|0}}} round 0}} minutes}}<noinclude> {{documentation}} <!-- PLEASE ADD CATEGORIES AND INTERWIKIS TO THE /doc SUBPAGE, THANKS --> </noinclude> d24309676bfe4692d038657a7171952e7d1cded7 Template:Template link expanded 10 75 142 2022-09-30T18:48:13Z dev>Pppery 0 wikitext text/x-wiki <code><nowiki>{{</nowiki>{{#if:{{{subst|}}} |[[Help:Substitution|subst]]:}}<!-- -->[[{{{sister|{{{SISTER|}}}}}}{{ns:Template}}:{{{1|}}}|{{{1|}}}]]<!-- -->{{#if:{{{2|}}} |&#124;{{{2}}}}}<!-- -->{{#if:{{{3|}}} |&#124;{{{3}}}}}<!-- -->{{#if:{{{4|}}} |&#124;{{{4}}}}}<!-- -->{{#if:{{{5|}}} |&#124;{{{5}}}}}<!-- -->{{#if:{{{6|}}} |&#124;{{{6}}}}}<!-- -->{{#if:{{{7|}}} |&#124;{{{7}}}}}<!-- -->{{#if:{{{8|}}} |&#124;{{{8}}}}}<!-- -->{{#if:{{{9|}}} |&#124;{{{9}}}}}<!-- -->{{#if:{{{10|}}} |&#124;{{{10}}}}}<!-- -->{{#if:{{{11|}}} |&#124;{{{11}}}}}<!-- -->{{#if:{{{12|}}} |&#124;{{{12}}}}}<!-- -->{{#if:{{{13|}}} |&#124;{{{13}}}}}<!-- -->{{#if:{{{14|}}} |&#124;{{{14}}}}}<!-- -->{{#if:{{{15|}}} |&#124;{{{15}}}}}<!-- -->{{#if:{{{16|}}} |&#124;{{{16}}}}}<!-- -->{{#if:{{{17|}}} |&#124;{{{17}}}}}<!-- -->{{#if:{{{18|}}} |&#124;{{{18}}}}}<!-- -->{{#if:{{{19|}}} |&#124;{{{19}}}}}<!-- -->{{#if:{{{20|}}} |&#124;{{{20}}}}}<!-- -->{{#if:{{{21|}}} |&#124;''...''}}<!-- --><nowiki>}}</nowiki></code><noinclude> {{Documentation}} </noinclude> 9f670205d4b358df089b1a820f78f02a88afca3a Template:User instagram 10 69 130 2022-10-01T17:13:01Z dev>Pppery 0 wikitext text/x-wiki {{Userbox | id = [[File:Instagram icon.png|37px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an {{#if:{{{account|}}}|&#32;account [https://instagram.com/{{{account}}} '''@{{{account}}}''']|account}} on Instagram. | nocat = {{{nocat|}}} | usercategory = Users who use Instagram }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> 1178ad0721de804c08dd554ebb4b52c4c6569fde Template:User IRC 10 65 122 2022-10-01T17:13:23Z dev>Pppery 0 wikitext text/x-wiki {{Userbox | id = # | id-s = 24 | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:IRC|IRC]]{{#if:{{{nick|}}}|&#32;as ''{{{nick}}}''|}}. | nocat = {{{nocat|}}} | usercategory = Users who use IRC }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> a148152ff16bb6fc7f7a7bd46c4898b50f1996fc Module:Yesno 828 95 182 2022-10-01T17:25:37Z dev>Pppery 0 Pppery moved page [[Module:Yesno/2]] to [[Module:Yesno]] without leaving a redirect Scribunto text/plain -- Function allowing for consistent treatment of boolean-like wikitext input. -- It works similarly to the template {{yesno}}. return function (val, default) -- If your wiki uses non-ascii characters for any of "yes", "no", etc., you -- should replace "val:lower()" with "mw.ustring.lower(val)" in the -- following line. val = type(val) == 'string' and val:lower() or val if val == nil then return nil elseif val == true or val == 'yes' or val == 'y' or val == 'true' or val == 't' or val == 'on' or tonumber(val) == 1 then return true elseif val == false or val == 'no' or val == 'n' or val == 'false' or val == 'f' or val == 'off' or tonumber(val) == 0 then return false else return default end end f767643e7d12126d020d88d662a3dd057817b9dc Module:No globals 828 87 166 2022-10-01T17:28:48Z dev>Pppery 0 Pppery moved page [[Module:No globals/2]] to [[Module:No globals]] without leaving a redirect Scribunto text/plain local mt = getmetatable(_G) or {} function mt.__index (t, k) if k ~= 'arg' then error('Tried to read nil global ' .. tostring(k), 2) end return nil end function mt.__newindex(t, k, v) if k ~= 'arg' then error('Tried to write global ' .. tostring(k), 2) end rawset(t, k, v) end setmetatable(_G, mt) 8ce3969f7d53b08bd00dabe4cc9780bc6afd412a Module:Documentation/config 828 22 37 2022-10-01T17:37:53Z dev>Pppery 0 Pppery moved page [[Module:Documentation/config/2]] to [[Module:Documentation/config]] without leaving a redirect Scribunto text/plain ---------------------------------------------------------------------------------------------------- -- -- Configuration for Module:Documentation -- -- Here you can set the values of the parameters and messages used in Module:Documentation to -- localise it to your wiki and your language. Unless specified otherwise, values given here -- should be string values. ---------------------------------------------------------------------------------------------------- local cfg = {} -- Do not edit this line. ---------------------------------------------------------------------------------------------------- -- Start box configuration ---------------------------------------------------------------------------------------------------- -- cfg['documentation-icon-wikitext'] -- The wikitext for the icon shown at the top of the template. cfg['documentation-icon-wikitext'] = '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]' -- cfg['template-namespace-heading'] -- The heading shown in the template namespace. cfg['template-namespace-heading'] = 'Template documentation' -- cfg['module-namespace-heading'] -- The heading shown in the module namespace. cfg['module-namespace-heading'] = 'Module documentation' -- cfg['file-namespace-heading'] -- The heading shown in the file namespace. cfg['file-namespace-heading'] = 'Summary' -- cfg['other-namespaces-heading'] -- The heading shown in other namespaces. cfg['other-namespaces-heading'] = 'Documentation' -- cfg['view-link-display'] -- The text to display for "view" links. cfg['view-link-display'] = 'view' -- cfg['edit-link-display'] -- The text to display for "edit" links. cfg['edit-link-display'] = 'edit' -- cfg['history-link-display'] -- The text to display for "history" links. cfg['history-link-display'] = 'history' -- cfg['purge-link-display'] -- The text to display for "purge" links. cfg['purge-link-display'] = 'purge' -- cfg['create-link-display'] -- The text to display for "create" links. cfg['create-link-display'] = 'create' ---------------------------------------------------------------------------------------------------- -- Link box (end box) configuration ---------------------------------------------------------------------------------------------------- -- cfg['transcluded-from-blurb'] -- Notice displayed when the docs are transcluded from another page. $1 is a wikilink to that page. cfg['transcluded-from-blurb'] = 'The above [[w:Wikipedia:Template documentation|documentation]] is [[mw:Help:Transclusion|transcluded]] from $1.' --[[ -- cfg['create-module-doc-blurb'] -- Notice displayed in the module namespace when the documentation subpage does not exist. -- $1 is a link to create the documentation page with the preload cfg['module-preload'] and the -- display cfg['create-link-display']. --]] cfg['create-module-doc-blurb'] = 'You might want to $1 a documentation page for this [[mw:Extension:Scribunto/Lua reference manual|Scribunto module]].' ---------------------------------------------------------------------------------------------------- -- Experiment blurb configuration ---------------------------------------------------------------------------------------------------- --[[ -- cfg['experiment-blurb-template'] -- cfg['experiment-blurb-module'] -- The experiment blurb is the text inviting editors to experiment in sandbox and test cases pages. -- It is only shown in the template and module namespaces. With the default English settings, it -- might look like this: -- -- Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages. -- -- In this example, "sandbox", "edit", "diff", "testcases", and "edit" would all be links. -- -- There are two versions, cfg['experiment-blurb-template'] and cfg['experiment-blurb-module'], depending -- on what namespace we are in. -- -- Parameters: -- -- $1 is a link to the sandbox page. If the sandbox exists, it is in the following format: -- -- cfg['sandbox-link-display'] (cfg['sandbox-edit-link-display'] | cfg['compare-link-display']) -- -- If the sandbox doesn't exist, it is in the format: -- -- cfg['sandbox-link-display'] (cfg['sandbox-create-link-display'] | cfg['mirror-link-display']) -- -- The link for cfg['sandbox-create-link-display'] link preloads the page with cfg['template-sandbox-preload'] -- or cfg['module-sandbox-preload'], depending on the current namespace. The link for cfg['mirror-link-display'] -- loads a default edit summary of cfg['mirror-edit-summary']. -- -- $2 is a link to the test cases page. If the test cases page exists, it is in the following format: -- -- cfg['testcases-link-display'] (cfg['testcases-edit-link-display'] | cfg['testcases-run-link-display']) -- -- If the test cases page doesn't exist, it is in the format: -- -- cfg['testcases-link-display'] (cfg['testcases-create-link-display']) -- -- If the test cases page doesn't exist, the link for cfg['testcases-create-link-display'] preloads the -- page with cfg['template-testcases-preload'] or cfg['module-testcases-preload'], depending on the current -- namespace. --]] cfg['experiment-blurb-template'] = "Editors can experiment in this template's $1 and $2 pages." cfg['experiment-blurb-module'] = "Editors can experiment in this module's $1 and $2 pages." ---------------------------------------------------------------------------------------------------- -- Sandbox link configuration ---------------------------------------------------------------------------------------------------- -- cfg['sandbox-subpage'] -- The name of the template subpage typically used for sandboxes. cfg['sandbox-subpage'] = 'sandbox' -- cfg['template-sandbox-preload'] -- Preload file for template sandbox pages. cfg['template-sandbox-preload'] = 'Template:Documentation/preload-sandbox' -- cfg['module-sandbox-preload'] -- Preload file for Lua module sandbox pages. cfg['module-sandbox-preload'] = 'Template:Documentation/preload-module-sandbox' -- cfg['sandbox-link-display'] -- The text to display for "sandbox" links. cfg['sandbox-link-display'] = 'sandbox' -- cfg['sandbox-edit-link-display'] -- The text to display for sandbox "edit" links. cfg['sandbox-edit-link-display'] = 'edit' -- cfg['sandbox-create-link-display'] -- The text to display for sandbox "create" links. cfg['sandbox-create-link-display'] = 'create' -- cfg['compare-link-display'] -- The text to display for "compare" links. cfg['compare-link-display'] = 'diff' -- cfg['mirror-edit-summary'] -- The default edit summary to use when a user clicks the "mirror" link. $1 is a wikilink to the -- template page. cfg['mirror-edit-summary'] = 'Create sandbox version of $1' -- cfg['mirror-link-display'] -- The text to display for "mirror" links. cfg['mirror-link-display'] = 'mirror' -- cfg['mirror-link-preload'] -- The page to preload when a user clicks the "mirror" link. cfg['mirror-link-preload'] = 'Template:Documentation/mirror' ---------------------------------------------------------------------------------------------------- -- Test cases link configuration ---------------------------------------------------------------------------------------------------- -- cfg['testcases-subpage'] -- The name of the template subpage typically used for test cases. cfg['testcases-subpage'] = 'testcases' -- cfg['template-testcases-preload'] -- Preload file for template test cases pages. cfg['template-testcases-preload'] = 'Template:Documentation/preload-testcases' -- cfg['module-testcases-preload'] -- Preload file for Lua module test cases pages. cfg['module-testcases-preload'] = 'Template:Documentation/preload-module-testcases' -- cfg['testcases-link-display'] -- The text to display for "testcases" links. cfg['testcases-link-display'] = 'testcases' -- cfg['testcases-edit-link-display'] -- The text to display for test cases "edit" links. cfg['testcases-edit-link-display'] = 'edit' -- cfg['testcases-run-link-display'] -- The text to display for test cases "run" links. cfg['testcases-run-link-display'] = 'run' -- cfg['testcases-create-link-display'] -- The text to display for test cases "create" links. cfg['testcases-create-link-display'] = 'create' ---------------------------------------------------------------------------------------------------- -- Add categories blurb configuration ---------------------------------------------------------------------------------------------------- --[[ -- cfg['add-categories-blurb'] -- Text to direct users to add categories to the /doc subpage. Not used if the "content" or -- "docname fed" arguments are set, as then it is not clear where to add the categories. $1 is a -- link to the /doc subpage with a display value of cfg['doc-link-display']. --]] cfg['add-categories-blurb'] = 'Add categories to the $1 subpage.' -- cfg['doc-link-display'] -- The text to display when linking to the /doc subpage. cfg['doc-link-display'] = '/doc' ---------------------------------------------------------------------------------------------------- -- Subpages link configuration ---------------------------------------------------------------------------------------------------- --[[ -- cfg['subpages-blurb'] -- The "Subpages of this template" blurb. $1 is a link to the main template's subpages with a -- display value of cfg['subpages-link-display']. In the English version this blurb is simply -- the link followed by a period, and the link display provides the actual text. --]] cfg['subpages-blurb'] = '$1.' --[[ -- cfg['subpages-link-display'] -- The text to display for the "subpages of this page" link. $1 is cfg['template-pagetype'], -- cfg['module-pagetype'] or cfg['default-pagetype'], depending on whether the current page is in -- the template namespace, the module namespace, or another namespace. --]] cfg['subpages-link-display'] = 'Subpages of this $1' -- cfg['template-pagetype'] -- The pagetype to display for template pages. cfg['template-pagetype'] = 'template' -- cfg['module-pagetype'] -- The pagetype to display for Lua module pages. cfg['module-pagetype'] = 'module' -- cfg['default-pagetype'] -- The pagetype to display for pages other than templates or Lua modules. cfg['default-pagetype'] = 'page' ---------------------------------------------------------------------------------------------------- -- Doc link configuration ---------------------------------------------------------------------------------------------------- -- cfg['doc-subpage'] -- The name of the subpage typically used for documentation pages. cfg['doc-subpage'] = 'doc' -- cfg['docpage-preload'] -- Preload file for template documentation pages in all namespaces. cfg['docpage-preload'] = 'Template:Documentation/preload' -- cfg['module-preload'] -- Preload file for Lua module documentation pages. cfg['module-preload'] = 'Template:Documentation/preload-module-doc' ---------------------------------------------------------------------------------------------------- -- HTML and CSS configuration ---------------------------------------------------------------------------------------------------- -- cfg['templatestyles'] -- The name of the TemplateStyles page where CSS is kept. -- Sandbox CSS will be at Module:Documentation/sandbox/styles.css when needed. cfg['templatestyles'] = 'Module:Documentation/styles.css' -- cfg['container'] -- Class which can be used to set flex or grid CSS on the -- two child divs documentation and documentation-metadata cfg['container'] = 'documentation-container' -- cfg['main-div-classes'] -- Classes added to the main HTML "div" tag. cfg['main-div-classes'] = 'documentation' -- cfg['main-div-heading-class'] -- Class for the main heading for templates and modules and assoc. talk spaces cfg['main-div-heading-class'] = 'documentation-heading' -- cfg['start-box-class'] -- Class for the start box cfg['start-box-class'] = 'documentation-startbox' -- cfg['start-box-link-classes'] -- Classes used for the [view][edit][history] or [create] links in the start box. -- mw-editsection-like is per [[Wikipedia:Village pump (technical)/Archive 117]] cfg['start-box-link-classes'] = 'mw-editsection-like plainlinks' -- cfg['end-box-class'] -- Class for the end box. cfg['end-box-class'] = 'documentation-metadata' -- cfg['end-box-plainlinks'] -- Plainlinks cfg['end-box-plainlinks'] = 'plainlinks' -- cfg['toolbar-class'] -- Class added for toolbar links. cfg['toolbar-class'] = 'documentation-toolbar' -- cfg['clear'] -- Just used to clear things. cfg['clear'] = 'documentation-clear' ---------------------------------------------------------------------------------------------------- -- Tracking category configuration ---------------------------------------------------------------------------------------------------- -- cfg['display-strange-usage-category'] -- Set to true to enable output of cfg['strange-usage-category'] if the module is used on a /doc subpage -- or a /testcases subpage. This should be a boolean value (either true or false). cfg['display-strange-usage-category'] = true -- cfg['strange-usage-category'] -- Category to output if cfg['display-strange-usage-category'] is set to true and the module is used on a -- /doc subpage or a /testcases subpage. cfg['strange-usage-category'] = 'Wikipedia pages with strange ((documentation)) usage' --[[ ---------------------------------------------------------------------------------------------------- -- End configuration -- -- Don't edit anything below this line. ---------------------------------------------------------------------------------------------------- --]] return cfg d70e8b1402a2bbe08a1fef4b75d743e661af0c95 Template:Documentation subpage 10 70 132 2022-10-01T17:51:17Z dev>Pppery 0 wikitext text/x-wiki <includeonly><!-- -->{{#ifeq:{{lc:{{SUBPAGENAME}}}} |{{{override|doc}}} | <!--(this template has been transcluded on a /doc or /{{{override}}} page)--> </includeonly><!-- -->{{#ifeq:{{{doc-notice|show}}} |show | {{Mbox | type = notice | style = margin-bottom:1.0em; | image = [[File:Edit-copy green.svg|40px|alt=|link=]] | text = '''This is a documentation subpage''' for '''{{{1|[[:{{SUBJECTSPACE}}:{{BASEPAGENAME}}]]}}}'''.<br/> It contains usage information, [[mw:Help:Categories|categories]] and other content that is not part of the original {{#if:{{{text2|}}} |{{{text2}}} |{{#if:{{{text1|}}} |{{{text1}}} | page}}}}. }} }}<!-- -->{{DEFAULTSORT:{{{defaultsort|{{PAGENAME}}}}}}}<!-- -->{{#if:{{{inhibit|}}} |<!--(don't categorize)--> | <includeonly><!-- -->{{#ifexist:{{NAMESPACE}}:{{BASEPAGENAME}} | [[Category:{{#switch:{{SUBJECTSPACE}} |Template=Template |Module=Module |User=User |#default=Wikipedia}} documentation pages]] | [[Category:Documentation subpages without corresponding pages]] }}<!-- --></includeonly> }}<!-- (completing initial #ifeq: at start of template:) --><includeonly> | <!--(this template has not been transcluded on a /doc or /{{{override}}} page)--> }}<!-- --></includeonly><noinclude>{{Documentation}}</noinclude> 471e685c1c643a5c6272e20e49824fffebad0448 Template:User youtube 10 68 128 2022-10-02T01:37:31Z dev>Tali64³ 0 Made social media template for YouTube wikitext text/x-wiki {{Userbox | id = [[File:YouTube full-color icon (2017).svg|37px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has a YouTube channel{{#if:{{{account|}}}|&#32;at [https://{{{account}}} '''{{{account}}}''']|}}. | nocat = {{{nocat|}}} | usercategory = Users who use YouTube }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> f0ba1080f2a2d69494317a9790fa3d7e6e4239b4 Template:User github 10 64 120 2022-10-04T16:14:11Z dev>MirahezeGDPR a51581232c7cc84ec1a32c40d8489548 0 More consistent with other userboxes + this is only supposed to be for accounts wikitext text/x-wiki {{Userbox | id = [[File:GitLogo.png|43px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an account on GitHub{{#if:{{{account|}}}|&#32;as ''[[github:{{{account}}}|{{{account}}}]]''|}}. | nocat = {{{nocat|}}} | usercategory = Users who use GitHub }}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> 08ef531d5b5a1e69b84939e0fc1f1d0d622f38ad Template:Hatnote 10 80 152 2022-10-05T21:18:12Z dev>Pppery 0 Category wikitext text/x-wiki <div style="margin-left:2em; margin-right: 2em;>''{{{1}}}''</div> <!-- The wikipedia templates uses :, which generated dd and dt tags. That is not ideal for accessibility. --><noinclude> This is a general purpose template for all kind of [https://en.wikipedia.org/wiki/Wikipedia:Hatnote hatnotes]. '''Hatnotes''' are a small annotation above a page or a section that can help the reader navigate. It's used for example to clarify what a section is about and link to other pages the reader may want to read. == Example usage == <pre><nowiki> === Heading === {{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}} The first sentence of the section is here. </nowiki></pre> === Heading === {{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}} The first sentence of the section is here. <templatedata> { "params": { "1": { "label": "content", "description": "the content of the note", "example": "For the xxx see [[yyy]]." } }, "description": "Adds a annotation for the reader about the content that follows. Usually used after a heading." } </templatedata> [[Category:Templates]] </noinclude> 5e58f83d6fa53ed06f85139184aff1d651804efe Template:MessageBox 10 72 136 2022-10-05T21:20:15Z dev>Pppery 0 Categorize wikitext text/x-wiki <div style="width: {{#if:{{{width|}}}|{{{width}}}|80%}}; background-color: {{#if:{{{Background color}}}|{{{Background color}}}|#f5f5f5}}; border-top: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-bottom: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-right: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-left: 12px solid {{#if:{{{Flag color}}}|{{{Flag color}}}|#aaaaaa}}; margin: 0.5em auto 0.5em;"> {| {{#if:{{{Image}}}|{{!}}style="width:93px; text-align:center; vertical-align:middle; padding-top:1px;padding-bottom:7px" {{!}} {{{Image}}} }} |style="vertical-align:middle;padding-left:3px;padding-top:10px;padding-bottom:10px;padding-right:10px; background-color: {{#if:{{{Background color}}}{{!}}{{{Background color}}}{{!}}#f5f5f5}};" | {{{Message text}}} |} </div><noinclude>[[Category:Notice templates]]</noinclude> c6727bf6179a36a5413ed93f232fd0e2f7180256 Template:Shortcut 10 73 138 2022-10-05T21:21:21Z dev>Pppery 0 Add doc wikitext text/x-wiki <!-- Putting anchors on page: --><div style="position: relative; top: -3em;">{{#if:{{{1|}}}|<span id="{{anchorencode:{{{1|}}}}}"></span> }}{{#if:{{{2|}}}|<span id="{{anchorencode:{{{2|}}}}}"></span> }}{{#if:{{{3|}}}|<span id="{{anchorencode:{{{3|}}}}}"></span> }}{{#if:{{{4|}}}|<span id="{{anchorencode:{{{4|}}}}}"></span> }}{{#if:{{{5|}}}|<span id="{{anchorencode:{{{5|}}}}}"></span> }}</div> <table class="shortcutbox noprint" style="float: right; border: 1px solid #aaa; background: #fff; margin: .3em .3em .3em 1em; padding: 3px; text-align: center;"><tr><th style="border: none; background: transparent;" class="plainlist"><!-- Adding the shortcut links: --><small>[[w:Wikipedia:Shortcut|Shortcut{{#if:{{{2|}}}|s}}]]: {{#if:{{{1|}}}|<ul><li> [[{{{1}}}]]</li> }}{{#if:{{{2|}}}|<li> [[{{{2}}}]]</li> }}{{#if:{{{3|}}}|<li> [[{{{3}}}]]</li> }}{{#if:{{{4|}}}|<li> [[{{{4}}}]]</li> }}{{#if:{{{5|}}}|<li> [[{{{5}}}]]</li> }}{{#if:{{{msg|}}}|<li> {{{msg}}}</li> }}</ul></small></th></tr></table><noinclude>{{doc}}</noinclude> 96a4c79718f5cbdf3e074cc545193ea4e863d1fb Template:Userbox 10 51 94 2022-10-05T21:25:59Z dev>Pppery 0 Already on doc wikitext text/x-wiki {{#invoke:userbox|userbox}}<noinclude>{{documentation}}</noinclude> 6813e8e31cadc62df2379b5fae9ea23c23f29e97 Module:Message box 828 90 172 2022-10-21T19:39:49Z dev>Pppery 0 These can just go, the first for being very Wikipedia-specific, and the second for being almost impossible to import properly Scribunto text/plain -- This is a meta-module for producing message box templates, including -- {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}. -- Load necessary modules. require('Module:No globals') local getArgs local yesno = require('Module:Yesno') local templatestyles = 'Module:Message box/styles.css' -- Get a language object for formatDate and ucfirst. local lang = mw.language.getContentLanguage() -- Define constants local CONFIG_MODULE = 'Module:Message box/configuration' local DEMOSPACES = {user = 'tmbox', talk = 'tmbox', image = 'imbox', file = 'imbox', category = 'cmbox', article = 'ambox', main = 'ambox'} -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function getTitleObject(...) -- Get the title object, passing the function through pcall -- in case we are over the expensive function count limit. local success, title = pcall(mw.title.new, ...) if success then return title end end local function union(t1, t2) -- Returns the union of two arrays. local vals = {} for i, v in ipairs(t1) do vals[v] = true end for i, v in ipairs(t2) do vals[v] = true end local ret = {} for k in pairs(vals) do table.insert(ret, k) end table.sort(ret) return ret end local function getArgNums(args, prefix) local nums = {} for k, v in pairs(args) do local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$') if num then table.insert(nums, tonumber(num)) end end table.sort(nums) return nums end -------------------------------------------------------------------------------- -- Box class definition -------------------------------------------------------------------------------- local MessageBox = {} MessageBox.__index = MessageBox function MessageBox.new(boxType, args, cfg) args = args or {} local obj = {} -- Set the title object and the namespace. obj.title = getTitleObject(args.page) or mw.title.getCurrentTitle() -- Set the config for our box type. obj.cfg = cfg[boxType] if not obj.cfg then local ns = obj.title.namespace -- boxType is "mbox" or invalid input if args.demospace and args.demospace ~= '' then -- implement demospace parameter of mbox local demospace = string.lower(args.demospace) if DEMOSPACES[demospace] then -- use template from DEMOSPACES obj.cfg = cfg[DEMOSPACES[demospace]] elseif string.find( demospace, 'talk' ) then -- demo as a talk page obj.cfg = cfg.tmbox else -- default to ombox obj.cfg = cfg.ombox end elseif ns == 0 then obj.cfg = cfg.ambox -- main namespace elseif ns == 6 then obj.cfg = cfg.imbox -- file namespace elseif ns == 14 then obj.cfg = cfg.cmbox -- category namespace else local nsTable = mw.site.namespaces[ns] if nsTable and nsTable.isTalk then obj.cfg = cfg.tmbox -- any talk namespace else obj.cfg = cfg.ombox -- other namespaces or invalid input end end end -- Set the arguments, and remove all blank arguments except for the ones -- listed in cfg.allowBlankParams. do local newArgs = {} for k, v in pairs(args) do if v ~= '' then newArgs[k] = v end end for i, param in ipairs(obj.cfg.allowBlankParams or {}) do newArgs[param] = args[param] end obj.args = newArgs end -- Define internal data structure. obj.categories = {} obj.classes = {} -- For lazy loading of [[Module:Category handler]]. obj.hasCategories = false return setmetatable(obj, MessageBox) end function MessageBox:addCat(ns, cat, sort) if not cat then return nil end if sort then cat = string.format('[[Category:%s|%s]]', cat, sort) else cat = string.format('[[Category:%s]]', cat) end self.hasCategories = true self.categories[ns] = self.categories[ns] or {} table.insert(self.categories[ns], cat) end function MessageBox:addClass(class) if not class then return nil end table.insert(self.classes, class) end function MessageBox:setParameters() local args = self.args local cfg = self.cfg -- Get type data. self.type = args.type local typeData = cfg.types[self.type] self.invalidTypeError = cfg.showInvalidTypeError and self.type and not typeData typeData = typeData or cfg.types[cfg.default] self.typeClass = typeData.class self.typeImage = typeData.image -- Find whether we are using a small message box. self.isSmall = cfg.allowSmall and ( cfg.smallParam and args.small == cfg.smallParam or not cfg.smallParam and yesno(args.small) ) -- Add attributes, classes and styles. self.id = args.id self.name = args.name if self.name then self:addClass('box-' .. string.gsub(self.name,' ','_')) end if yesno(args.plainlinks) ~= false then self:addClass('plainlinks') end for _, class in ipairs(cfg.classes or {}) do self:addClass(class) end if self.isSmall then self:addClass(cfg.smallClass or 'mbox-small') end self:addClass(self.typeClass) self:addClass(args.class) self.style = args.style self.attrs = args.attrs -- Set text style. self.textstyle = args.textstyle -- Find if we are on the template page or not. This functionality is only -- used if useCollapsibleTextFields is set, or if both cfg.templateCategory -- and cfg.templateCategoryRequireName are set. self.useCollapsibleTextFields = cfg.useCollapsibleTextFields if self.useCollapsibleTextFields or cfg.templateCategory and cfg.templateCategoryRequireName then if self.name then local templateName = mw.ustring.match( self.name, '^[tT][eE][mM][pP][lL][aA][tT][eE][%s_]*:[%s_]*(.*)$' ) or self.name templateName = 'Template:' .. templateName self.templateTitle = getTitleObject(templateName) end self.isTemplatePage = self.templateTitle and mw.title.equals(self.title, self.templateTitle) end -- Process data for collapsible text fields. At the moment these are only -- used in {{ambox}}. if self.useCollapsibleTextFields then -- Get the self.issue value. if self.isSmall and args.smalltext then self.issue = args.smalltext else local sect if args.sect == '' then sect = 'This ' .. (cfg.sectionDefault or 'page') elseif type(args.sect) == 'string' then sect = 'This ' .. args.sect end local issue = args.issue issue = type(issue) == 'string' and issue ~= '' and issue or nil local text = args.text text = type(text) == 'string' and text or nil local issues = {} table.insert(issues, sect) table.insert(issues, issue) table.insert(issues, text) self.issue = table.concat(issues, ' ') end -- Get the self.talk value. local talk = args.talk -- Show talk links on the template page or template subpages if the talk -- parameter is blank. if talk == '' and self.templateTitle and ( mw.title.equals(self.templateTitle, self.title) or self.title:isSubpageOf(self.templateTitle) ) then talk = '#' elseif talk == '' then talk = nil end if talk then -- If the talk value is a talk page, make a link to that page. Else -- assume that it's a section heading, and make a link to the talk -- page of the current page with that section heading. local talkTitle = getTitleObject(talk) local talkArgIsTalkPage = true if not talkTitle or not talkTitle.isTalkPage then talkArgIsTalkPage = false talkTitle = getTitleObject( self.title.text, mw.site.namespaces[self.title.namespace].talk.id ) end if talkTitle and talkTitle.exists then local talkText = 'Relevant discussion may be found on' if talkArgIsTalkPage then talkText = string.format( '%s [[%s|%s]].', talkText, talk, talkTitle.prefixedText ) else talkText = string.format( '%s the [[%s#%s|talk page]].', talkText, talkTitle.prefixedText, talk ) end self.talk = talkText end end -- Get other values. self.fix = args.fix ~= '' and args.fix or nil local date if args.date and args.date ~= '' then date = args.date elseif args.date == '' and self.isTemplatePage then date = lang:formatDate('F Y') end if date then self.date = string.format(" <small class='date-container'>''(<span class='date'>%s</span>)''</small>", date) end self.info = args.info end -- Set the non-collapsible text field. At the moment this is used by all box -- types other than ambox, and also by ambox when small=yes. if self.isSmall then self.text = args.smalltext or args.text else self.text = args.text end -- Set the below row. self.below = cfg.below and args.below -- General image settings. self.imageCellDiv = not self.isSmall and cfg.imageCellDiv self.imageEmptyCell = cfg.imageEmptyCell if cfg.imageEmptyCellStyle then self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px' end -- Left image settings. local imageLeft = self.isSmall and args.smallimage or args.image if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none' or not cfg.imageCheckBlank and imageLeft ~= 'none' then self.imageLeft = imageLeft if not imageLeft then local imageSize = self.isSmall and (cfg.imageSmallSize or '30x30px') or '40x40px' self.imageLeft = string.format('[[File:%s|%s|link=|alt=]]', self.typeImage or 'Imbox notice.png', imageSize) end end -- Right image settings. local imageRight = self.isSmall and args.smallimageright or args.imageright if not (cfg.imageRightNone and imageRight == 'none') then self.imageRight = imageRight end end function MessageBox:setMainspaceCategories() local args = self.args local cfg = self.cfg if not cfg.allowMainspaceCategories then return nil end local nums = {} for _, prefix in ipairs{'cat', 'category', 'all'} do args[prefix .. '1'] = args[prefix] nums = union(nums, getArgNums(args, prefix)) end -- The following is roughly equivalent to the old {{Ambox/category}}. local date = args.date date = type(date) == 'string' and date local preposition = 'from' for _, num in ipairs(nums) do local mainCat = args['cat' .. tostring(num)] or args['category' .. tostring(num)] local allCat = args['all' .. tostring(num)] mainCat = type(mainCat) == 'string' and mainCat allCat = type(allCat) == 'string' and allCat if mainCat and date and date ~= '' then local catTitle = string.format('%s %s %s', mainCat, preposition, date) self:addCat(0, catTitle) catTitle = getTitleObject('Category:' .. catTitle) if not catTitle or not catTitle.exists then self:addCat(0, 'Articles with invalid date parameter in template') end elseif mainCat and (not date or date == '') then self:addCat(0, mainCat) end if allCat then self:addCat(0, allCat) end end end function MessageBox:setTemplateCategories() local args = self.args local cfg = self.cfg -- Add template categories. if cfg.templateCategory then if cfg.templateCategoryRequireName then if self.isTemplatePage then self:addCat(10, cfg.templateCategory) end elseif not self.title.isSubpage then self:addCat(10, cfg.templateCategory) end end -- Add template error categories. if cfg.templateErrorCategory then local templateErrorCategory = cfg.templateErrorCategory local templateCat, templateSort if not self.name and not self.title.isSubpage then templateCat = templateErrorCategory elseif self.isTemplatePage then local paramsToCheck = cfg.templateErrorParamsToCheck or {} local count = 0 for i, param in ipairs(paramsToCheck) do if not args[param] then count = count + 1 end end if count > 0 then templateCat = templateErrorCategory templateSort = tostring(count) end if self.categoryNums and #self.categoryNums > 0 then templateCat = templateErrorCategory templateSort = 'C' end end self:addCat(10, templateCat, templateSort) end end function MessageBox:setAllNamespaceCategories() -- Set categories for all namespaces. if self.invalidTypeError then local allSort = (self.title.namespace == 0 and 'Main:' or '') .. self.title.prefixedText self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort) end end function MessageBox:setCategories() if self.title.namespace == 0 then self:setMainspaceCategories() elseif self.title.namespace == 10 then self:setTemplateCategories() end self:setAllNamespaceCategories() end function MessageBox:renderCategories() if not self.hasCategories then -- No categories added, no need to pass them to Category handler so, -- if it was invoked, it would return the empty string. -- So we shortcut and return the empty string. return "" end -- Convert category tables to strings and pass them through -- [[Module:Category handler]]. return require('Module:Category handler')._main{ main = table.concat(self.categories[0] or {}), template = table.concat(self.categories[10] or {}), all = table.concat(self.categories.all or {}), nocat = self.args.nocat, page = self.args.page } end function MessageBox:export() local root = mw.html.create() -- Create the box table. local boxTable = root:tag('table') boxTable:attr('id', self.id or nil) for i, class in ipairs(self.classes or {}) do boxTable:addClass(class or nil) end boxTable :cssText(self.style or nil) :attr('role', 'presentation') if self.attrs then boxTable:attr(self.attrs) end -- Add the left-hand image. local row = boxTable:tag('tr') if self.imageLeft then local imageLeftCell = row:tag('td'):addClass('mbox-image') if self.imageCellDiv then -- If we are using a div, redefine imageLeftCell so that the image -- is inside it. Divs use style="width: 52px;", which limits the -- image width to 52px. If any images in a div are wider than that, -- they may overlap with the text or cause other display problems. imageLeftCell = imageLeftCell:tag('div'):css('width', '52px') end imageLeftCell:wikitext(self.imageLeft or nil) elseif self.imageEmptyCell then -- Some message boxes define an empty cell if no image is specified, and -- some don't. The old template code in templates where empty cells are -- specified gives the following hint: "No image. Cell with some width -- or padding necessary for text cell to have 100% width." row:tag('td') :addClass('mbox-empty-cell') :cssText(self.imageEmptyCellStyle or nil) end -- Add the text. local textCell = row:tag('td'):addClass('mbox-text') if self.useCollapsibleTextFields then -- The message box uses advanced text parameters that allow things to be -- collapsible. At the moment, only ambox uses this. textCell:cssText(self.textstyle or nil) local textCellDiv = textCell:tag('div') textCellDiv :addClass('mbox-text-span') :wikitext(self.issue or nil) if (self.talk or self.fix) and not self.isSmall then textCellDiv:tag('span') :addClass('hide-when-compact') :wikitext(self.talk and (' ' .. self.talk) or nil) :wikitext(self.fix and (' ' .. self.fix) or nil) end textCellDiv:wikitext(self.date and (' ' .. self.date) or nil) if self.info and not self.isSmall then textCellDiv :tag('span') :addClass('hide-when-compact') :wikitext(self.info and (' ' .. self.info) or nil) end else -- Default text formatting - anything goes. textCell :cssText(self.textstyle or nil) :wikitext(self.text or nil) end -- Add the right-hand image. if self.imageRight then local imageRightCell = row:tag('td'):addClass('mbox-imageright') if self.imageCellDiv then -- If we are using a div, redefine imageRightCell so that the image -- is inside it. imageRightCell = imageRightCell:tag('div'):css('width', '52px') end imageRightCell :wikitext(self.imageRight or nil) end -- Add the below row. if self.below then boxTable:tag('tr') :tag('td') :attr('colspan', self.imageRight and '3' or '2') :addClass('mbox-text') :cssText(self.textstyle or nil) :wikitext(self.below or nil) end -- Add error message for invalid type parameters. if self.invalidTypeError then root:tag('div') :css('text-align', 'center') :wikitext(string.format( 'This message box is using an invalid "type=%s" parameter and needs fixing.', self.type or '' )) end -- Add categories. root:wikitext(self:renderCategories() or nil) return tostring(root) end -------------------------------------------------------------------------------- -- Exports -------------------------------------------------------------------------------- local p, mt = {}, {} function p._exportClasses() -- For testing. return { MessageBox = MessageBox } end function p.main(boxType, args, cfgTables) local box = MessageBox.new(boxType, args, cfgTables or mw.loadData(CONFIG_MODULE)) box:setParameters() box:setCategories() return box:export() end local function templatestyles(frame, src) return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = templatestyles} } .. 'CONFIG_MODULE' end function mt.__index(t, k) return function (frame) if not getArgs then getArgs = require('Module:Arguments').getArgs end return t.main(k, getArgs(frame, {trim = false, removeBlanks = false})) end end return setmetatable(p, mt) be00cd389f9f2afcd361e5d5e33622839555cbd9 Template:Para 10 58 108 2022-10-21T19:52:33Z dev>Pppery 0 wikitext text/x-wiki <code class="tpl-para" style="word-break:break-word;{{SAFESUBST:<noinclude />#if:{{{plain|}}}|border: none; background-color: inherit;}} {{SAFESUBST:<noinclude />#if:{{{style|}}}|{{{style}}}}}">&#124;{{SAFESUBST:<noinclude />#if:{{{1|}}}|{{{1}}}&#61;}}{{{2|}}}</code><noinclude> {{Documentation}} <!--Categories and interwikis go near the bottom of the /doc subpage.--> </noinclude> 7be5bee75307eae9342bbb9ff3a613e93e93d5a7 Module:Message box/configuration 828 91 174 2022-10-21T22:38:02Z dev>Pppery 0 Scribunto text/plain -------------------------------------------------------------------------------- -- Message box configuration -- -- -- -- This module contains configuration data for [[Module:Message box]]. -- -------------------------------------------------------------------------------- return { ambox = { types = { speedy = { class = 'ambox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'ambox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'ambox-content', image = 'Ambox important.svg' }, style = { class = 'ambox-style', image = 'Edit-clear.svg' }, move = { class = 'ambox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'ambox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'ambox-notice', image = 'Information icon4.svg' } }, default = 'notice', allowBlankParams = {'talk', 'sect', 'date', 'issue', 'fix', 'hidden'}, allowSmall = true, smallParam = 'left', smallClass = 'mbox-small-left', classes = {'metadata', 'ambox'}, imageEmptyCell = true, imageCheckBlank = true, imageSmallSize = '20x20px', imageCellDiv = true, useCollapsibleTextFields = true, imageRightNone = true, sectionDefault = 'article', allowMainspaceCategories = true, templateCategory = 'Article message templates', templateCategoryRequireName = true, templateErrorCategory = 'Article message templates with missing parameters', templateErrorParamsToCheck = {'issue', 'fix'}, }, cmbox = { types = { speedy = { class = 'cmbox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'cmbox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'cmbox-content', image = 'Ambox important.svg' }, style = { class = 'cmbox-style', image = 'Edit-clear.svg' }, move = { class = 'cmbox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'cmbox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'cmbox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'cmbox'}, imageEmptyCell = true }, fmbox = { types = { warning = { class = 'fmbox-warning', image = 'Ambox warning pn.svg' }, editnotice = { class = 'fmbox-editnotice', image = 'Information icon4.svg' }, system = { class = 'fmbox-system', image = 'Information icon4.svg' } }, default = 'system', showInvalidTypeError = true, classes = {'fmbox'}, imageEmptyCell = false, imageRightNone = false }, imbox = { types = { speedy = { class = 'imbox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'imbox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'imbox-content', image = 'Ambox important.svg' }, style = { class = 'imbox-style', image = 'Edit-clear.svg' }, move = { class = 'imbox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'imbox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, license = { class = 'imbox-license licensetpl', image = 'Imbox license.png' -- @todo We need an SVG version of this }, featured = { class = 'imbox-featured', image = 'Cscr-featured.svg' }, notice = { class = 'imbox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'imbox'}, imageEmptyCell = true, below = true, templateCategory = 'File message boxes' }, ombox = { types = { speedy = { class = 'ombox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'ombox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'ombox-content', image = 'Ambox important.svg' }, style = { class = 'ombox-style', image = 'Edit-clear.svg' }, move = { class = 'ombox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'ombox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'ombox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'ombox'}, allowSmall = true, imageEmptyCell = true, imageRightNone = true }, tmbox = { types = { speedy = { class = 'tmbox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'tmbox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'tmbox-content', image = 'Ambox important.svg' }, style = { class = 'tmbox-style', image = 'Edit-clear.svg' }, move = { class = 'tmbox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'tmbox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'tmbox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'tmbox'}, allowSmall = true, imageRightNone = true, imageEmptyCell = true, imageEmptyCellStyle = true, templateCategory = 'Talk message boxes' } } c6bd9191861b23e474e12b19c694335c4bc3af5f Template:Mbox 10 71 134 2022-10-21T23:02:23Z dev>Pppery 0 Reverted edits by [[Special:Contributions/Pppery|Pppery]] ([[User talk:Pppery|talk]]) to last revision by [[User:wikipedia>Amorymeltzer|wikipedia>Amorymeltzer]] wikitext text/x-wiki {{#invoke:Message box|mbox}}<noinclude> {{documentation}} <!-- Categories go on the /doc subpage, and interwikis go on Wikidata. --> </noinclude> c262e205f85f774a23f74119179ceea11751d68e Template:Delete 10 50 92 2022-10-24T15:28:45Z dev>Unknown user 0 Update tvar tags wikitext text/x-wiki {{MessageBox |Flag color=firebrick |Border color=firebrick |Background color=#FFEEEE |Image=[[File:Trash Can.svg|80px]] |Message text=<span style="line-height:2;"><span style="color:red; line-height:1.2;">'''This article is a candidate for speedy deletion because {{{1}}}. '''</span><br><span style="color:#000000;"> Deleting Reason: {{{1|No reason given}}}</span></span> }}<noinclude>[[Category:Notice templates]]</noinclude> <includeonly>[[Category:Candidates for deletion]]</includeonly> <noinclude> <languages /> <translate> <!--T:1--> == Usage Note == Add this template to any page on this wiki for which you're requesting an [[<tvar name=admin>mw:Special:MyLanguage/Manual:Administrators</tvar>|administrator]] to delete, either by adding it to the very top of the page (preferred) or by replacing the existing content with this template (also acceptable) following the format prescribed below.</translate> <code><nowiki>{{Delete|1=</nowiki>''Your deletion reason''<nowiki>}}</nowiki></code> <translate> <!--T:2--> Replace ''your deletion reason'' with one of the commonly accepted reasons for deletion below, or describe concisely ''why'' you are requesting deletion. <!--T:3--> If you do not specify a reason in parameter <code>1=</code>, ''no deletion reason'' will be inserted, and your request ''may'' be declined if it is not apparent why deletion is being requested. <!--T:4--> === Commonly accepted reasons for deletion === * Vandalism * Attack page/page created solely for harassment * Copyright violation * Spam * Test page. Please either use the [[<tvar name=sb>m:Meta:Sandbox</tvar>|community sandbox]] or [[<tvar name=mypsb>Special:MyPage/sandbox</tvar>|create your personal sandbox]] * Non-controversial housekeeping * [[<tvar name=br>Special:BrokenRedirects</tvar>|Broken redirect]] * [[<tvar name=dr>Special:DoubleRedirects</tvar>|Double redirect]] * Author requests deletion, or author blanked * Subpages with no parent page * Talk pages with no companion page and no meaningful discussion history * Images available as identical copies on either [[<tvar name=commons>commons:Special:MyLanguage/Main Page</tvar>|Miraheze Commons]] or [[wikimediacommons:|Wikimedia Commons]] * [[<tvar name=uc>Special:UnusedCategories</tvar>|Empty category]] * [[<tvar name=ut>Special:UnusedTemplates</tvar>|Unused template (including a template redirect)]] with no inlinks, transclusions, or page watchers === Parameter(s) === <!--T:5--> </translate> <templatedata> { "params": { "1": { "label": "Deletion reason", "example": "Author requests deletion, or author blanked", "default": "No deletion reason", "suggested": true, "description": "Template to add to any page requiring deletion." } } } </templatedata> </noinclude> ae465d4609d3bbb646339e90ae469c31ce34a9df Template:Current time 10 78 148 2022-11-01T19:57:46Z dev>Pppery 0 Oops wikitext text/x-wiki {{#switch:{{{1}}} |Coordinated Universal Time=Current UTC is {{CURRENTTIME}} |UTC-1=Current time for {{{1}}} is {{utc|23}} |UTC-2=Current time for {{{1}}} is {{utc|22}} |UTC-2:30=Current time for {{{1}}} is {{utc|21|30}} |UTC-3=Current time for {{{1}}} is {{utc|21}} |UTC-3:30=Current time for {{{1}}} is {{utc|20|30}} |UTC-4=Current time for {{{1}}} is {{utc|20}} |UTC-5=Current time for {{{1}}} is {{utc|19}} |UTC-6=Current time for {{{1}}} is {{utc|18}} |UTC-7=Current time for {{{1}}} is {{utc|17}} |UTC-8=Current time for {{{1}}} is {{utc|16}} |UTC-9=Current time for {{{1}}} is {{utc|15}} |UTC-9:30=Current time for {{{1}}} is {{utc|14|30}} |UTC-10=Current time for {{{1}}} is {{utc|14}} |UTC-11=Current time for {{{1}}} is {{utc|13}} |UTC-12=Current time for {{{1}}} is {{utc|12}} |UTC+0:20=Current time for {{{1}}} is {{utc|0|20}} |UTC+0:30=Current time for {{{1}}} is {{utc|0|30}} |UTC+1=Current time for {{{1}}} is {{utc|1}} |UTC+2=Current time for {{{1}}} is {{utc|2}} |UTC+3=Current time for {{{1}}} is {{utc|3}} |UTC+3:30=Current time for {{{1}}} is {{utc|3|30}} |UTC+4=Current time for {{{1}}} is {{utc|4}} |UTC+4:30=Current time for {{{1}}} is {{utc|4|30}} |UTC+4:51=Current time for {{{1}}} is {{utc|4|51}} |UTC+5=Current time for {{{1}}} is {{utc|5}} |UTC+5:30=Current time for {{{1}}} is {{utc|5|30}} |UTC+5:40=Current time for {{{1}}} is {{utc|5|40}} |UTC+5:45=Current time for {{{1}}} is {{utc|5|45}} |UTC+6=Current time for {{{1}}} is {{utc|6}} |UTC+6:30=Current time for {{{1}}} is {{utc|6|30}} |UTC+7=Current time for {{{1}}} is {{utc|7}} |UTC+7:20=Current time for {{{1}}} is {{utc|7|20}} |UTC+7:30=Current time for {{{1}}} is {{utc|7|30}} |UTC+8=Current time for {{{1}}} is {{utc|8}} |UTC+8:30=Current time for {{{1}}} is {{utc|8|30}} |UTC+8:45=Current time for {{{1}}} is {{utc|8|45}} |UTC+9=Current time for {{{1}}} is {{utc|9}} |UTC+9:30=Current time for {{{1}}} is {{utc|9|30}} |UTC+10=Current time for {{{1}}} is {{utc|10}} |UTC+10:30=Current time for {{{1}}} is {{utc|10|30}} |UTC+11=Current time for {{{1}}} is {{utc|11}} |UTC+11:30=Current time for {{{1}}} is {{utc|11|30}} |UTC+12=Current time for {{{1}}} is {{utc|12}} |UTC+12:45=Current time for {{{1}}} is {{utc|12|45}} |UTC+13=Current time for {{{1}}} is {{utc|13}} |UTC+13:45=Current time for {{{1}}} is {{utc|13|45}} |UTC+14=Current time for {{{1}}} is {{utc|14}} |#default=Current time is {{CURRENTTIME}} }}<noinclude>{{documentation|content=Returns the current time in a given timezone (defaulting to the timezone specified in [[Special:ManageWiki/settings#mw-section-localisation]], which in turn defaults to UTC) == Examples == {{tlx|current time}} -> {{current time}} {{tlx|current time|UTC+1}} -> {{current time|UTC+1}} {{tlx|current time|UTC-5}} -> {{current time|UTC-5}} [[Category:Templates]] }}</noinclude> 84d7f12dbea154240f9fa86372863cd6152dd98b Template:Description missing 10 81 154 2022-11-01T20:03:48Z dev>Pppery 0 Use documentation wikitext text/x-wiki <div class="boilerplate metadata" id="cleanup" style="text-align: center; background: #ffe; margin: .75em 15%; padding: .5em; border: 1px solid #e3e3b0;"> This media has no '''{{ #if: {{{1|}}} | {{{1}}} | description }}''', and may be lacking other information. <br> Media should have a summary to inform others of the content, author, source, and date if possible. If you know or have access to such information, please add it to the image page. </div> <includeonly>{{#switch:{{NAMESPACE}}|{{ns:6}}=|#default={{#ifeq:{{{category|}}}|no||[[Category:Images lacking a description|{{PAGENAME}}]]}}}}</includeonly><noinclude> {{documentation}} </noinclude> 2b5026cefd37c307f7f2ee331289c38741f834a5 Template:Discussion top 10 76 144 2022-11-01T20:08:13Z dev>Pppery 0 Add documentation wikitext text/x-wiki <div class="boilerplate metadata discussion-archived" style="background-color: #F2F4FC; margin: 2em 0 0 0; padding: 0 10px 0 10px; border: 1px solid #aaa"> :The following discussion is closed. Please do not modify it. Subsequent comments should be made in a new section. ::{{{1|}}} ----<noinclude></div>{{documentation}}</noinclude> c8b38525e188dbfa68b0e9cdd1864ceff2ed100e Template:Discussion bottom 10 77 146 2022-11-01T20:09:03Z dev>Pppery 0 Add documentation wikitext text/x-wiki <noinclude><div></noinclude>---- :The above discussion is preserved as an archive. Please do not modify it. Subsequent comments should be made in a new section </div><noinclude>{{documentation|Template:Discussion top/doc}}</noinclude> 80d5baa979985b3b685585611b0e954d2c1c6e10 Template:Uses TemplateStyles 10 85 162 2022-11-07T02:43:20Z dev>Pppery 0 3 revisions imported from [[:wikipedia:Template:Uses_TemplateStyles]]: Importing this now because the Wikipedia version will fail in a non-obvious way (a problem I caught on another Miraheze wiki) wikitext text/x-wiki <includeonly>{{#invoke:Uses TemplateStyles|main}}</includeonly><noinclude> {{Uses TemplateStyles|Template:Uses TemplateStyles/example.css|nocat=true}} {{documentation}} <!-- Categories go on the /doc subpage and interwikis go on Wikidata. --> </noinclude> 7e26d8f257e302bd8a3dcbe53f52741ae0884f74 Module:Uses TemplateStyles 828 86 164 2022-11-07T02:51:31Z dev>Pppery 0 Scribunto text/plain -- This module implements the {{Uses TemplateStyles}} template. local mMessageBox = require('Module:Message box') local p = {} function p.main(frame) local origArgs = frame:getParent().args local args = {} for k, v in pairs(origArgs) do v = v:match('^%s*(.-)%s*$') if v ~= '' then args[k] = v end end return p._main(args) end function p._main(args) return p.renderBox(args) end function p.renderBox(tStyles) local boxArgs = {} if #tStyles < 1 then boxArgs.text = '<strong class="error">Error: no TemplateStyles specified</strong>' else local tStylesLinks = {} for i, ts in ipairs(tStyles) do local sandboxLink = nil local tsTitle = mw.title.new(ts) if tsTitle then local tsSandboxTitle = mw.title.new(string.format('%s:%s/sandbox/%s', tsTitle.nsText, tsTitle.baseText, tsTitle.subpageText)) if tsSandboxTitle and tsSandboxTitle.exists then sandboxLink = string.format(' ([[:%s|sandbox]])', tsSandboxTitle.prefixedText) end end tStylesLinks[i] = string.format('[[:%s]]%s', ts, sandboxLink or '') end local tStylesList = mw.text.listToText(tStylesLinks) boxArgs.text = 'This ' .. (mw.title.getCurrentTitle():inNamespaces(828,829) and 'module' or 'template') .. ' uses [[mw:Extension:TemplateStyles|TemplateStyles]]:\n' .. tStylesList end boxArgs.type = 'notice' boxArgs.small = true boxArgs.image = '[[File:Farm-Fresh css add.svg|32px|alt=CSS]]' return mMessageBox.main('mbox', boxArgs) end return p 3c7364ddaba9beb17a73b0f5256cd7fc3b3051f4 Template:Header 10 23 39 2022-12-16T04:46:16Z dev>Pppery 0 wikitext text/x-wiki {| style="width: 100% !important;" |- | style="border-top: 4px solid #{{{topbarhex|6F6F6F}}}; background-color: #{{{bodyhex|F6F6F6}}}; padding: 10px 15px;" | {{#if:{{{shortcut|}}}| {{shortcut|{{{shortcut|uselang={{{uselang|{{CURRENTCONTENTLANGUAGE}}}}}}}}}}}}<div style="font-size:180%; text-align: left; color: {{{titlecolor|}}}">'''{{{title|{{{1|{{BASEPAGENAME}}}}}}}}'''</div> <div style="padding-top:0.3em; padding-bottom:0.1em; font-size:100%; text-align: left; color: {{{bodycolor|}}}">{{{notes|Put some notes here!}}}</div> |- | style="height: 10px" | |} {{clear}}<noinclude>{{documentation}}[[Category:templates]]</noinclude> 03aac86137ab11bfccbcceb2de919475af2953dd Template:Documentation/mirror 10 46 84 2022-12-16T17:01:57Z dev>Pppery 0 wikitext text/x-wiki <includeonly>{{subst:msgnw:{{subst:NAMESPACE}}:{{subst:BASEPAGENAME}}}}</includeonly><noinclude>{{doc}}</noinclude> 2f3df1c981931719e821f054f3db0c67072f781e Template:Documentation/preload-sandbox 10 48 88 2022-12-16T17:04:46Z dev>Pppery 0 7 revisions imported from [[:wikipedia:Template:Documentation/preload-sandbox]] wikitext text/x-wiki <!-- Add your experimental template code here. --><noin<includeonly></includeonly>clude> {{Documentation}} </noin<includeonly></includeonly>clude> c7b7f3f85c510513f7415b512c03742fe61ee31a Template:Documentation/preload-testcases 10 49 90 2022-12-16T17:08:51Z dev>Pppery 0 Created page with "This page contains test cases for {{tlx|{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}}}.<noinclude>{{documentation|content=This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template testcases page.}}[[Category:Documentation preloads]]</noinclude>" wikitext text/x-wiki This page contains test cases for {{tlx|{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}}}.<noinclude>{{documentation|content=This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template testcases page.}}[[Category:Documentation preloads]]</noinclude> 9e9c7d43373bd7aeaee3293842ae54db0257e549 Template:Documentation/preload 10 47 86 2022-12-16T17:09:58Z dev>Pppery 0 wikitext text/x-wiki {{Documentation subpage}} == Usage == <include<includeonly></includeonly>only> <!-- Categories below this line --> }}</include<includeonly></includeonly>only><noinclude> {{Documentation|content= This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template documentation page. [[Category:Documentation preloads]] }}</noinclude> 587413cafd960a3f7f3c9257a4160f3654fdbe0f Template:See also 10 56 104 2023-01-10T01:56:06Z dev>Pppery 0 Revert to version by Wikipedia->bkonrad wikitext text/x-wiki {{hatnote|extraclasses=boilerplate seealso|{{{altphrase|See also}}}: {{#if:{{{1<includeonly>|</includeonly>}}} |<!--then:-->[[:{{{1}}}{{#if:{{{label 1|{{{l1|}}}}}}|{{!}}{{{label 1|{{{l1}}}}}}}}]] |<!--else:-->'''Error: [[Template:See also|Template must be given at least one article name]]''' }}{{#if:{{{2|}}}|{{#if:{{{3|}}}|, |&nbsp;and }} [[:{{{2}}}{{#if:{{{label 2|{{{l2|}}}}}}|{{!}}{{{label 2|{{{l2}}}}}}}}]] }}{{#if:{{{3|}}}|{{#if:{{{4|}}}|, |,&nbsp;and }} [[:{{{3}}}{{#if:{{{label 3|{{{l3|}}}}}}|{{!}}{{{label 3|{{{l3}}}}}}}}]] }}{{#if:{{{4|}}}|{{#if:{{{5|}}}|, |,&nbsp;and }} [[:{{{4}}}{{#if:{{{label 4|{{{l4|}}}}}}|{{!}}{{{label 4|{{{l4}}}}}}}}]] }}{{#if:{{{5|}}}|{{#if:{{{6|}}}|, |,&nbsp;and }} [[:{{{5}}}{{#if:{{{label 5|{{{l5|}}}}}}|{{!}}{{{label 5|{{{l5}}}}}}}}]] }}{{#if:{{{6|}}}|{{#if:{{{7|}}}|, |,&nbsp;and }} [[:{{{6}}}{{#if:{{{label 6|{{{l6|}}}}}}|{{!}}{{{label 6|{{{l6}}}}}}}}]] }}{{#if:{{{7|}}}|{{#if:{{{8|}}}|, |,&nbsp;and }} [[:{{{7}}}{{#if:{{{label 7|{{{l7|}}}}}}|{{!}}{{{label 7|{{{l7}}}}}}}}]] }}{{#if:{{{8|}}}|{{#if:{{{9|}}}|, |,&nbsp;and }} [[:{{{8}}}{{#if:{{{label 8|{{{l8|}}}}}}|{{!}}{{{label 8|{{{l8}}}}}}}}]] }}{{#if:{{{9|}}}|{{#if:{{{10|}}}|, |,&nbsp;and }} [[:{{{9}}}{{#if:{{{label 9|{{{l9|}}}}}}|{{!}}{{{label 9|{{{l9}}}}}}}}]] }}{{#if:{{{10|}}}|{{#if:{{{11|}}}|, |,&nbsp;and }} [[:{{{10}}}{{#if:{{{label 10|{{{l10|}}}}}}|{{!}}{{{label 10|{{{l10}}}}}}}}]] }}{{#if:{{{11|}}}|{{#if:{{{12|}}}|, |,&nbsp;and }} [[:{{{11}}}{{#if:{{{label 11|{{{l11|}}}}}}|{{!}}{{{label 11|{{{l11}}}}}}}}]] }}{{#if:{{{12|}}}|{{#if:{{{13|}}}|, |,&nbsp;and }} [[:{{{12}}}{{#if:{{{label 12|{{{l12|}}}}}}|{{!}}{{{label 12|{{{l12}}}}}}}}]] }}{{#if:{{{13|}}}|{{#if:{{{14|}}}|, |,&nbsp;and }} [[:{{{13}}}{{#if:{{{label 13|{{{l13|}}}}}}|{{!}}{{{label 13|{{{l13}}}}}}}}]] }}{{#if:{{{14|}}}|{{#if:{{{15|}}}|, |,&nbsp;and }} [[:{{{14}}}{{#if:{{{label 14|{{{l14|}}}}}}|{{!}}{{{label 14|{{{l14}}}}}}}}]] }}{{#if:{{{15|}}}|,&nbsp;and [[:{{{15}}}{{#if:{{{label 15|{{{l15|}}} }}}|{{!}}{{{label 15|{{{l15|}}} }}} }}]] }}{{#if:{{{16|}}}| &mdash; '''<br/>Error: [[Template:See also|Too many links specified (maximum is 15)]]''' }}}}<noinclude> {{documentation}} </noinclude> 0315f43d7e4b679054955c7a50fe554ab1df63de Module:TNT 828 92 176 2023-01-10T02:02:55Z dev>Pppery 0 Scribunto text/plain -- -- INTRO: (!!! DO NOT RENAME THIS PAGE !!!) -- This module allows any template or module to be copy/pasted between -- wikis without any translation changes. All translation text is stored -- in the global Data:*.tab pages on Commons, and used everywhere. -- -- SEE: https://www.mediawiki.org/wiki/Multilingual_Templates_and_Modules -- -- ATTENTION: -- Please do NOT rename this module - it has to be identical on all wikis. -- This code is maintained at https://www.mediawiki.org/wiki/Module:TNT -- Please do not modify it anywhere else, as it may get copied and override your changes. -- Suggestions can be made at https://www.mediawiki.org/wiki/Module_talk:TNT -- -- DESCRIPTION: -- The "msg" function uses a Commons dataset to translate a message -- with a given key (e.g. source-table), plus optional arguments -- to the wiki markup in the current content language. -- Use lang=xx to set language. Example: -- -- {{#invoke:TNT | msg -- | I18n/Template:Graphs.tab <!-- https://commons.wikimedia.org/wiki/Data:I18n/Template:Graphs.tab --> -- | source-table <!-- uses a translation message with id = "source-table" --> -- | param1 }} <!-- optional parameter --> -- -- -- The "doc" function will generate the <templatedata> parameter documentation for templates. -- This way all template parameters can be stored and localized in a single Commons dataset. -- NOTE: "doc" assumes that all documentation is located in Data:Templatedata/* on Commons. -- -- {{#invoke:TNT | doc | Graph:Lines }} -- uses https://commons.wikimedia.org/wiki/Data:Templatedata/Graph:Lines.tab -- if the current page is Template:Graph:Lines/doc -- local p = {} local i18nDataset = 'I18n/Module:TNT.tab' -- Forward declaration of the local functions local sanitizeDataset, loadData, link, formatMessage function p.msg(frame) local dataset, id local params = {} local lang = nil for k, v in pairs(frame.args) do if k == 1 then dataset = mw.text.trim(v) elseif k == 2 then id = mw.text.trim(v) elseif type(k) == 'number' then params[k - 2] = mw.text.trim(v) elseif k == 'lang' and v ~= '_' then lang = mw.text.trim(v) end end return formatMessage(dataset, id, params, lang) end -- Identical to p.msg() above, but used from other lua modules -- Parameters: name of dataset, message key, optional arguments -- Example with 2 params: format('I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset') function p.format(dataset, key, ...) local checkType = require('libraryUtil').checkType checkType('format', 1, dataset, 'string') checkType('format', 2, key, 'string') return formatMessage(dataset, key, {...}) end -- Identical to p.msg() above, but used from other lua modules with the language param -- Parameters: language code, name of dataset, message key, optional arguments -- Example with 2 params: formatInLanguage('es', I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset') function p.formatInLanguage(lang, dataset, key, ...) local checkType = require('libraryUtil').checkType checkType('formatInLanguage', 1, lang, 'string') checkType('formatInLanguage', 2, dataset, 'string') checkType('formatInLanguage', 3, key, 'string') return formatMessage(dataset, key, {...}, lang) end -- Obsolete function that adds a 'c:' prefix to the first param. -- "Sandbox/Sample.tab" -> 'c:Data:Sandbox/Sample.tab' function p.link(frame) return link(frame.args[1]) end function p.doc(frame) local dataset = 'Templatedata/' .. sanitizeDataset(frame.args[1]) return frame:extensionTag('templatedata', p.getTemplateData(dataset)) .. formatMessage(i18nDataset, 'edit_doc', {link(dataset)}) end function p.getTemplateData(dataset) -- TODO: add '_' parameter once lua starts reindexing properly for "all" languages local data = loadData(dataset) local names = {} for _, field in ipairs(data.schema.fields) do table.insert(names, field.name) end local params = {} local paramOrder = {} for _, row in ipairs(data.data) do local newVal = {} local name = nil for pos, columnName in ipairs(names) do if columnName == 'name' then name = row[pos] else newVal[columnName] = row[pos] end end if name then params[name] = newVal table.insert(paramOrder, name) end end -- Work around json encoding treating {"1":{...}} as an [{...}] params['zzz123']='' local json = mw.text.jsonEncode({ params=params, paramOrder=paramOrder, description=data.description }) json = string.gsub(json,'"zzz123":"",?', "") return json end -- Local functions sanitizeDataset = function(dataset) if not dataset then return nil end dataset = mw.text.trim(dataset) if dataset == '' then return nil elseif string.sub(dataset,-4) ~= '.tab' then return dataset .. '.tab' else return dataset end end loadData = function(dataset, lang) dataset = sanitizeDataset(dataset) if not dataset then error(formatMessage(i18nDataset, 'error_no_dataset', {})) end -- Give helpful error to thirdparties who try and copy this module. if not mw.ext or not mw.ext.data or not mw.ext.data.get then error(string.format([['''Missing JsonConfig extension, or not properly configured; Cannot load https://commons.wikimedia.org/wiki/Data:%s. Please properly enable the JSONConfig extension at Special:ManageWiki/extensions#mw-prefsection-other See https://www.mediawiki.org/wiki/Extension:JsonConfig#Supporting_Wikimedia_templates''']], dataset)) end local data = mw.ext.data.get(dataset, lang) if data == false then if dataset == i18nDataset then -- Prevent cyclical calls error('Missing Commons dataset ' .. i18nDataset) else error(formatMessage(i18nDataset, 'error_bad_dataset', {link(dataset)})) end end return data end -- Given a dataset name, convert it to a title with the 'commons:data:' prefix link = function(dataset) return 'c:Data:' .. mw.text.trim(dataset or '') end formatMessage = function(dataset, key, params, lang) for _, row in pairs(loadData(dataset, lang).data) do local id, msg = unpack(row) if id == key then local result = mw.message.newRawMessage(msg, unpack(params or {})) return result:plain() end end if dataset == i18nDataset then -- Prevent cyclical calls error('Invalid message key "' .. key .. '"') else error(formatMessage(i18nDataset, 'error_bad_msgkey', {key, link(dataset)})) end end return p 6d981852d69d5958a60d96d24c311680564c6103 Template:Pending 10 39 70 2023-01-16T07:23:51Z dev>BrandonWM 0 from meta wikitext text/x-wiki {{On hold|{{{1|Pending}}}}}<noinclude>{{Documentation}}</noinclude> 3d534f8f2cf14f73be843d306efcecbff05c7f5e Template:Endorse 10 31 55 2023-01-16T07:24:17Z dev>BrandonWM 0 from meta wikitext text/x-wiki [[File:Symbol support2 vote.svg|link=|alt=|16px|]]&nbsp;'''{{{1|Endorse}}}'''<noinclude>{{Documentation}}</noinclude> 23cc6c948818ca6949bd9af0991af58f1858483f Template:Partly done 10 38 68 2023-01-16T07:26:51Z dev>BrandonWM 0 from meta wikitext text/x-wiki <span class="nowrap">[[File:Yellow_check.svg|18px|link=|alt=]]&nbsp;'''{{{1|Partly done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|&#58; {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 24a90b5a5c4c716b7ec12889fbd09a1da2ba1ca3 Template:Resolved 10 41 74 2023-01-16T07:28:00Z dev>BrandonWM 0 from meta wikitext text/x-wiki <span class="nowrap">[[File:Yes check.svg|18px|link=]]&nbsp;'''{{{1|Resolved}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|&#58; {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> bcebb832c81fc395e8891f82747510f76292cb34 Template:Agree 10 24 41 2023-01-16T07:29:51Z dev>BrandonWM 0 from meta wikitext text/x-wiki [[File:Symbol confirmed.svg|18px|link=]] '''{{{1|Agree}}}'''<noinclude>{{documentation}}</noinclude> 775ddedaccda0d477a1b3c82d422e3760c862609 Template:Working 10 45 82 2023-01-16T07:30:45Z dev>BrandonWM 0 from meta wikitext text/x-wiki [[File:Icon tools.svg|20px|link=]]&nbsp;'''{{{1|Working}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 0619210f08d5114b9a348b4f1045a0b6f4552012 Template:Idea 10 33 59 2023-01-16T07:31:31Z dev>BrandonWM 0 from meta wikitext text/x-wiki [[File:Dialog-information on.svg|18px|link=]] '''{{{1|Idea}}}:'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> e4062daed60634ce9e9cd2f052d9102bcf7e2916 Template:Reviewing 10 42 76 2023-01-16T07:34:11Z dev>BrandonWM 0 add from meta wikitext text/x-wiki [[File:Pictogram voting wait green.svg|17px|link=]] '''{{{1|Reviewing}}}...'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 0184f75a66f991d9eb99f23a75df36dd184e0c4b Template:Note 10 30 53 2023-01-16T07:34:58Z dev>BrandonWM 0 Created page with "[[File:Pictogram voting info.svg|18px|link=]]&nbsp;'''{{{1|Note:}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude>" wikitext text/x-wiki [[File:Pictogram voting info.svg|18px|link=]]&nbsp;'''{{{1|Note:}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 4d5cae62908f9cc8da2988712b236fe939bc80e2 Template:Question 10 40 72 2023-01-16T07:35:38Z dev>BrandonWM 0 meta wikitext text/x-wiki [[File:Pictogram voting question.svg|18px|link=]]&nbsp;'''{{{1|Question:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude> 9fae3d5ccc70d95a5a7de8983d7a82c1a55853e3 Template:High priority 10 32 57 2023-01-16T07:36:41Z dev>BrandonWM 0 meta wikitext text/x-wiki [[File:Exclamationdiamond.svg|20px|link=]]&nbsp;'''{{{1|High Priority}}}'''{{{{{|safesubst:}}}#if:{{{note|{{{reason|}}}}}}|<nowiki />: {{{note|{{{reason}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 65d49ca7f928fef46651d89d894267497560a60b Template:Thank you 10 43 78 2023-01-16T07:37:23Z dev>BrandonWM 0 meta wikitext text/x-wiki <span class="nowrap">[[File:Face-smile.svg|18px|link=]] '''{{{1|Thank you}}}'''</span><noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 4312420b6485d1eb316af5c56f663a7d618afb9b Template:In progress 10 34 61 2023-01-16T22:21:50Z dev>Pppery 0 Pppery moved page [[Template:In Progress]] to [[Template:In progress]] wikitext text/x-wiki [[File:Pictogram voting info.svg|16px|link=|alt=]]&nbsp;'''{{{1|In Progress}}}'''<noinclude>{{documentation}}</noinclude> ff9e3ff4245b3dcc9ae45a1f8e3c7e7830fc0fff Template:Done 10 29 51 2023-01-16T22:22:56Z dev>Pppery 0 Reverted edits by [[Special:Contributions/BrandonWM|BrandonWM]] ([[User talk:BrandonWM|talk]]) to last revision by MacFan4000 wikitext text/x-wiki <span class="nowrap">[[File:Yes check.svg|18px|link=|alt=]]&nbsp;'''{{{1|Done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|&#58; {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude> {{documentation}} [[Category:Resolution templates]]</noinclude> 717c1385d516cd84dc05a10ba88359a52c9d8415 Template:Withdrawn 10 44 80 2023-01-16T22:50:23Z dev>Pppery 0 wikitext text/x-wiki [[File:Cancelled process mini.svg|200x20px|link=|alt=]]&nbsp;'''{{{1|Request withdrawn}}}'''<noinclude>{{documentation}}</noinclude> 24c0cd218d3a61ac8b524c6f8d1b5cc405ca3d80 Template:Custom resolution 10 27 47 2023-01-16T22:51:28Z dev>Pppery 0 wikitext text/x-wiki <span class="nowrap">[[File:{{{1|Cancelled process mini.svg}}}|18px|alt={{{2|Text here}}}]] <span style="{{{3|">'''{{{2|Text here}}}'''</span></span> <noinclude>{{Documentation|content= This template allows for the creation of custom [[Template:Template list#Resolution templates|resolution templates]] using 2 parameters. }}[[Category:Resolution templates]]</noinclude> a563b1f700886c4f97480a7ee81988b33af01ccf Template:Information/style.css 10 53 98 2023-01-16T23:32:31Z dev>Pppery 0 text text/plain .fileinfo-paramfield { background: #ccf; text-align: right; padding-right: 0.4em; width: 15%; font-weight: bold; } /* [[Category:Template stylesheets]] */ 396fcf8276bedcc9dad608bdbd9bf1be7f90424d Module:Documentation/styles.css 828 97 185 2023-01-16T23:40:04Z dev>Pppery 0 text text/plain .documentation, .documentation-metadata { border: 1px solid #a2a9b1; background-color: #ecfcf4; clear: both; } .documentation { margin: 1em 0 0 0; padding: 1em; } .documentation-metadata { margin: 0.2em 0; /* same margin left-right as .documentation */ font-style: italic; padding: 0.4em 1em; /* same padding left-right as .documentation */ } .documentation-startbox { padding-bottom: 3px; border-bottom: 1px solid #aaa; margin-bottom: 1ex; } .documentation-heading { font-weight: bold; font-size: 125%; } .documentation-clear { /* Don't want things to stick out where they shouldn't. */ clear: both; } .documentation-toolbar { font-style: normal; font-size: 85%; } /* [[Category:Template stylesheets]] */ 5fb984fe8632dc068db16853a824c9f3d5175dd9 Module:String 828 93 178 2023-01-20T02:43:41Z dev>Pppery 0 And this one too Scribunto text/plain --[[ This module is intended to provide access to basic string functions. Most of the functions provided here can be invoked with named parameters, unnamed parameters, or a mixture. If named parameters are used, Mediawiki will automatically remove any leading or trailing whitespace from the parameter. Depending on the intended use, it may be advantageous to either preserve or remove such whitespace. Global options ignore_errors: If set to 'true' or 1, any error condition will result in an empty string being returned rather than an error message. error_category: If an error occurs, specifies the name of a category to include with the error message. The default category is [Category:Errors reported by Module String]. no_category: If set to 'true' or 1, no category will be added if an error is generated. Unit tests for this module are available at Module:String/tests. ]] local str = {} --[[ len This function returns the length of the target string. Usage: {{#invoke:String|len|target_string|}} OR {{#invoke:String|len|s=target_string}} Parameters s: The string whose length to report If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from the target string. ]] function str.len( frame ) local new_args = str._getParameters( frame.args, {'s'} ) local s = new_args['s'] or '' return mw.ustring.len( s ) end --[[ sub This function returns a substring of the target string at specified indices. Usage: {{#invoke:String|sub|target_string|start_index|end_index}} OR {{#invoke:String|sub|s=target_string|i=start_index|j=end_index}} Parameters s: The string to return a subset of i: The fist index of the substring to return, defaults to 1. j: The last index of the string to return, defaults to the last character. The first character of the string is assigned an index of 1. If either i or j is a negative value, it is interpreted the same as selecting a character by counting from the end of the string. Hence, a value of -1 is the same as selecting the last character of the string. If the requested indices are out of range for the given string, an error is reported. ]] function str.sub( frame ) local new_args = str._getParameters( frame.args, { 's', 'i', 'j' } ) local s = new_args['s'] or '' local i = tonumber( new_args['i'] ) or 1 local j = tonumber( new_args['j'] ) or -1 local len = mw.ustring.len( s ) -- Convert negatives for range checking if i < 0 then i = len + i + 1 end if j < 0 then j = len + j + 1 end if i > len or j > len or i < 1 or j < 1 then return str._error( 'String subset index out of range' ) end if j < i then return str._error( 'String subset indices out of order' ) end return mw.ustring.sub( s, i, j ) end --[[ _match This function returns a substring from the source string that matches a specified pattern. It is exported for use in other modules Usage: strmatch = require("Module:String")._match sresult = strmatch( s, pattern, start, match, plain, nomatch ) Parameters s: The string to search pattern: The pattern or string to find within the string start: The index within the source string to start the search. The first character of the string has index 1. Defaults to 1. match: In some cases it may be possible to make multiple matches on a single string. This specifies which match to return, where the first match is match= 1. If a negative number is specified then a match is returned counting from the last match. Hence match = -1 is the same as requesting the last match. Defaults to 1. plain: A flag indicating that the pattern should be understood as plain text. Defaults to false. nomatch: If no match is found, output the "nomatch" value rather than an error. For information on constructing Lua patterns, a form of [regular expression], see: * http://www.lua.org/manual/5.1/manual.html#5.4.1 * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns ]] -- This sub-routine is exported for use in other modules function str._match( s, pattern, start, match_index, plain_flag, nomatch ) if s == '' then return str._error( 'Target string is empty' ) end if pattern == '' then return str._error( 'Pattern string is empty' ) end start = tonumber(start) or 1 if math.abs(start) < 1 or math.abs(start) > mw.ustring.len( s ) then return str._error( 'Requested start is out of range' ) end if match_index == 0 then return str._error( 'Match index is out of range' ) end if plain_flag then pattern = str._escapePattern( pattern ) end local result if match_index == 1 then -- Find first match is simple case result = mw.ustring.match( s, pattern, start ) else if start > 1 then s = mw.ustring.sub( s, start ) end local iterator = mw.ustring.gmatch(s, pattern) if match_index > 0 then -- Forward search for w in iterator do match_index = match_index - 1 if match_index == 0 then result = w break end end else -- Reverse search local result_table = {} local count = 1 for w in iterator do result_table[count] = w count = count + 1 end result = result_table[ count + match_index ] end end if result == nil then if nomatch == nil then return str._error( 'Match not found' ) else return nomatch end else return result end end --[[ match This function returns a substring from the source string that matches a specified pattern. Usage: {{#invoke:String|match|source_string|pattern_string|start_index|match_number|plain_flag|nomatch_output}} OR {{#invoke:String|match|s=source_string|pattern=pattern_string|start=start_index |match=match_number|plain=plain_flag|nomatch=nomatch_output}} Parameters s: The string to search pattern: The pattern or string to find within the string start: The index within the source string to start the search. The first character of the string has index 1. Defaults to 1. match: In some cases it may be possible to make multiple matches on a single string. This specifies which match to return, where the first match is match= 1. If a negative number is specified then a match is returned counting from the last match. Hence match = -1 is the same as requesting the last match. Defaults to 1. plain: A flag indicating that the pattern should be understood as plain text. Defaults to false. nomatch: If no match is found, output the "nomatch" value rather than an error. If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from each string. In some circumstances this is desirable, in other cases one may want to preserve the whitespace. If the match_number or start_index are out of range for the string being queried, then this function generates an error. An error is also generated if no match is found. If one adds the parameter ignore_errors=true, then the error will be suppressed and an empty string will be returned on any failure. For information on constructing Lua patterns, a form of [regular expression], see: * http://www.lua.org/manual/5.1/manual.html#5.4.1 * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns ]] -- This is the entry point for #invoke:String|match function str.match( frame ) local new_args = str._getParameters( frame.args, {'s', 'pattern', 'start', 'match', 'plain', 'nomatch'} ) local s = new_args['s'] or '' local start = tonumber( new_args['start'] ) or 1 local plain_flag = str._getBoolean( new_args['plain'] or false ) local pattern = new_args['pattern'] or '' local match_index = math.floor( tonumber(new_args['match']) or 1 ) local nomatch = new_args['nomatch'] return str._match( s, pattern, start, match_index, plain_flag, nomatch ) end --[[ pos This function returns a single character from the target string at position pos. Usage: {{#invoke:String|pos|target_string|index_value}} OR {{#invoke:String|pos|target=target_string|pos=index_value}} Parameters target: The string to search pos: The index for the character to return If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from the target string. In some circumstances this is desirable, in other cases one may want to preserve the whitespace. The first character has an index value of 1. If one requests a negative value, this function will select a character by counting backwards from the end of the string. In other words pos = -1 is the same as asking for the last character. A requested value of zero, or a value greater than the length of the string returns an error. ]] function str.pos( frame ) local new_args = str._getParameters( frame.args, {'target', 'pos'} ) local target_str = new_args['target'] or '' local pos = tonumber( new_args['pos'] ) or 0 if pos == 0 or math.abs(pos) > mw.ustring.len( target_str ) then return str._error( 'String index out of range' ) end return mw.ustring.sub( target_str, pos, pos ) end --[[ find This function allows one to search for a target string or pattern within another string. Usage: {{#invoke:String|find|source_str|target_string|start_index|plain_flag}} OR {{#invoke:String|find|source=source_str|target=target_str|start=start_index|plain=plain_flag}} Parameters source: The string to search target: The string or pattern to find within source start: The index within the source string to start the search, defaults to 1 plain: Boolean flag indicating that target should be understood as plain text and not as a Lua style regular expression, defaults to true If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from the parameter. In some circumstances this is desirable, in other cases one may want to preserve the whitespace. This function returns the first index >= "start" where "target" can be found within "source". Indices are 1-based. If "target" is not found, then this function returns 0. If either "source" or "target" are missing / empty, this function also returns 0. This function should be safe for UTF-8 strings. ]] function str.find( frame ) local new_args = str._getParameters( frame.args, {'source', 'target', 'start', 'plain' } ) local source_str = new_args['source'] or '' local pattern = new_args['target'] or '' local start_pos = tonumber(new_args['start']) or 1 local plain = new_args['plain'] or true if source_str == '' or pattern == '' then return 0 end plain = str._getBoolean( plain ) local start = mw.ustring.find( source_str, pattern, start_pos, plain ) if start == nil then start = 0 end return start end --[[ replace This function allows one to replace a target string or pattern within another string. Usage: {{#invoke:String|replace|source_str|pattern_string|replace_string|replacement_count|plain_flag}} OR {{#invoke:String|replace|source=source_string|pattern=pattern_string|replace=replace_string| count=replacement_count|plain=plain_flag}} Parameters source: The string to search pattern: The string or pattern to find within source replace: The replacement text count: The number of occurences to replace, defaults to all. plain: Boolean flag indicating that pattern should be understood as plain text and not as a Lua style regular expression, defaults to true ]] function str.replace( frame ) local new_args = str._getParameters( frame.args, {'source', 'pattern', 'replace', 'count', 'plain' } ) local source_str = new_args['source'] or '' local pattern = new_args['pattern'] or '' local replace = new_args['replace'] or '' local count = tonumber( new_args['count'] ) local plain = new_args['plain'] or true if source_str == '' or pattern == '' then return source_str end plain = str._getBoolean( plain ) if plain then pattern = str._escapePattern( pattern ) replace = mw.ustring.gsub( replace, "%%", "%%%%" ) --Only need to escape replacement sequences. end local result if count ~= nil then result = mw.ustring.gsub( source_str, pattern, replace, count ) else result = mw.ustring.gsub( source_str, pattern, replace ) end return result end --[[ simple function to pipe string.rep to templates. ]] function str.rep( frame ) local repetitions = tonumber( frame.args[2] ) if not repetitions then return str._error( 'function rep expects a number as second parameter, received "' .. ( frame.args[2] or '' ) .. '"' ) end return string.rep( frame.args[1] or '', repetitions ) end --[[ escapePattern This function escapes special characters from a Lua string pattern. See [1] for details on how patterns work. [1] https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns Usage: {{#invoke:String|escapePattern|pattern_string}} Parameters pattern_string: The pattern string to escape. ]] function str.escapePattern( frame ) local pattern_str = frame.args[1] if not pattern_str then return str._error( 'No pattern string specified' ) end local result = str._escapePattern( pattern_str ) return result end --[[ count This function counts the number of occurrences of one string in another. ]] function str.count(frame) local args = str._getParameters(frame.args, {'source', 'pattern', 'plain'}) local source = args.source or '' local pattern = args.pattern or '' local plain = str._getBoolean(args.plain or true) if plain then pattern = str._escapePattern(pattern) end local _, count = mw.ustring.gsub(source, pattern, '') return count end --[[ endswith This function determines whether a string ends with another string. ]] function str.endswith(frame) local args = str._getParameters(frame.args, {'source', 'pattern'}) local source = args.source or '' local pattern = args.pattern or '' if pattern == '' then -- All strings end with the empty string. return "yes" end if mw.ustring.sub(source, -mw.ustring.len(pattern), -1) == pattern then return "yes" else return "" end end --[[ join Join all non empty arguments together; the first argument is the separator. Usage: {{#invoke:String|join|sep|one|two|three}} ]] function str.join(frame) local args = {} local sep for _, v in ipairs( frame.args ) do if sep then if v ~= '' then table.insert(args, v) end else sep = v end end return table.concat( args, sep or '' ) end --[[ Helper function that populates the argument list given that user may need to use a mix of named and unnamed parameters. This is relevant because named parameters are not identical to unnamed parameters due to string trimming, and when dealing with strings we sometimes want to either preserve or remove that whitespace depending on the application. ]] function str._getParameters( frame_args, arg_list ) local new_args = {} local index = 1 local value for _, arg in ipairs( arg_list ) do value = frame_args[arg] if value == nil then value = frame_args[index] index = index + 1 end new_args[arg] = value end return new_args end --[[ Helper function to handle error messages. ]] function str._error( error_str ) local frame = mw.getCurrentFrame() local error_category = frame.args.error_category or 'Errors reported by Module String' local ignore_errors = frame.args.ignore_errors or false local no_category = frame.args.no_category or false if str._getBoolean(ignore_errors) then return '' end local error_str = '<strong class="error">String Module Error: ' .. error_str .. '</strong>' if error_category ~= '' and not str._getBoolean( no_category ) then error_str = '[[Category:' .. error_category .. ']]' .. error_str end return error_str end --[[ Helper Function to interpret boolean strings ]] function str._getBoolean( boolean_str ) local boolean_value if type( boolean_str ) == 'string' then boolean_str = boolean_str:lower() if boolean_str == 'false' or boolean_str == 'no' or boolean_str == '0' or boolean_str == '' then boolean_value = false else boolean_value = true end elseif type( boolean_str ) == 'boolean' then boolean_value = boolean_str else error( 'No boolean value found' ) end return boolean_value end --[[ Helper function that escapes all pattern characters so that they will be treated as plain text. ]] function str._escapePattern( pattern_str ) return mw.ustring.gsub( pattern_str, "([%(%)%.%%%+%-%*%?%[%^%$%]])", "%%%1" ) end return str 73c9d229ca32cb5e05a3873238b69fec347cf4b1 Template:Soft redirect 10 55 102 2023-01-20T02:48:04Z dev>Pppery 0 Drop categorization ([[customizing templattes|an assumption about the structure of the wiki]]), and clean up wikitext text/x-wiki __NONEWSECTIONLINK__[[File:Softredirarrow.svg|64px|Soft redirect to:|link=]]<span class="redirectText" id="softredirect">[[:{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}|{{{2|{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}}}}]]</span><br /><span style="font-size:85%; padding-left:48px;">This page is a [[metawikimedia:soft redirect|soft redirect]].</span><noinclude> {{Documentation}} </noinclude> a965c0fe43aa0fe8f0e17ed40d725f0e7b3649f6 MediaWiki:Twinkle.js 8 84 160 2023-02-01T23:54:44Z dev>Pppery 0 javascript text/javascript /* twinkle js file [[Category:Twinkle]] loads all dependencies then twinkle */ mw.loader.getScript( 'https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.js?action=raw&ctype=text/javascript' ).then( function () {mw.loader.load(["mediawiki.util"]);mw.loader.load(["jquery.ui"]);mw.loader.load(["jquery.tipsy"]);mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.css?action=raw&ctype=text/css", "text/css");mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-Twinkle.js?action=raw&ctype=text/javascript");}, function ( e ) {mw.log.error( e.message );} ); a49ff2e963c82cee4e45c90883102d1e662bd8b8 MediaWiki:Gadget-Twinkle.js 8 83 158 2023-02-03T01:06:43Z dev>Pppery 0 Reverted edits by [[Special:Contributions/Pppery|Pppery]] ([[User talk:Pppery|talk]]) to last revision by Naleksuh javascript text/javascript /** Twinkle.js [[Category:Twinkle]] Forked from simplewiki's version of Twinkle and de-Wikipedia-fied by Naleksuh Currently in beta and still has some reseblences to Wikipedia. Will be more fine-tuned over time */ //<nowiki> mw.loader.load("https://dev.miraheze.org/w/index.php?title=MediaWiki:Jquerymigrate-3.3.2.js&action=raw&ctype=text/javascript"); ( function ( window, document, $, undefined ) { // Wrap with anonymous function var Twinkle = {}; window.Twinkle = Twinkle; // allow global access // for use by custom modules (normally empty) Twinkle.initCallbacks = []; Twinkle.addInitCallback = function twinkleAddInitCallback( func ) { Twinkle.initCallbacks.push( func ); }; Twinkle.defaultConfig = {}; /** * Twinkle.defaultConfig.twinkle and Twinkle.defaultConfig.friendly * * This holds the default set of preferences used by Twinkle. (The |friendly| object holds preferences stored in the FriendlyConfig object.) * It is important that all new preferences added here, especially admin-only ones, are also added to * |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel. * For help on the actual preferences, see the comments in twinkleconfig.js. */ Twinkle.defaultConfig.twinkle = { // General summaryAd: " ([[mh:dev:Twinkle|TW]])", deletionSummaryAd: " ([[mh:dev:Twinkle|TW]])", protectionSummaryAd: " ([[mh:dev:Twinkle|TW]])", userTalkPageMode: "window", dialogLargeFont: false, // Fluff (revert and rollback) openTalkPage: [ "agf", "norm", "vand" ], openTalkPageOnAutoRevert: false, markRevertedPagesAsMinor: [ "vand" ], watchRevertedPages: [ "agf", "norm", "vand", "torev" ], offerReasonOnNormalRevert: true, confirmOnFluff: false, showRollbackLinks: [ "diff", "others" ], // CSD speedySelectionStyle: "buttonClick", speedyPromptOnG7: false, watchSpeedyPages: [ "g3", "g5", "g10", "g11", "g12" ], markSpeedyPagesAsPatrolled: true, // these next two should probably be identical by default notifyUserOnSpeedyDeletionNomination: [ ], welcomeUserOnSpeedyDeletionNotification: [ ], promptForSpeedyDeletionSummary: [ "db" ], openUserTalkPageOnSpeedyDelete: [ "db"], deleteTalkPageOnDelete: false, deleteSysopDefaultToTag: false, speedyWindowHeight: 500, speedyWindowWidth: 800, logSpeedyNominations: false, speedyLogPageName: "Deletion request log", noLogOnSpeedyNomination: [ "u1" ], // Unlink unlinkNamespaces: [ "0" ], // Warn defaultWarningGroup: "1", showSharedIPNotice: true, watchWarnings: true, blankTalkpageOnIndefBlock: false, // XfD xfdWatchDiscussion: "default", xfdWatchList: "no", xfdWatchPage: "default", xfdWatchUser: "default", // Hidden preferences revertMaxRevisions: 50, batchdeleteChunks: 50, batchDeleteMinCutOff: 5, batchMax: 5000, batchProtectChunks: 50, batchProtectMinCutOff: 5, batchundeleteChunks: 50, batchUndeleteMinCutOff: 5 }; // now some skin dependent config. if ( mw.config.get( "skin" ) === "vector" || mw.config.get("skin") === "vector-2022") { Twinkle.defaultConfig.twinkle.portletArea = "right-navigation"; Twinkle.defaultConfig.twinkle.portletId = "p-twinkle"; Twinkle.defaultConfig.twinkle.portletName = "TW"; Twinkle.defaultConfig.twinkle.portletType = "menu"; Twinkle.defaultConfig.twinkle.portletNext = "p-search"; } else { Twinkle.defaultConfig.twinkle.portletArea = null; Twinkle.defaultConfig.twinkle.portletId = "p-cactions"; Twinkle.defaultConfig.twinkle.portletName = null; Twinkle.defaultConfig.twinkle.portletType = null; Twinkle.defaultConfig.twinkle.portletNext = null; } Twinkle.defaultConfig.friendly = { // Tag groupByDefault: true, watchTaggedPages: true, markTaggedPagesAsMinor: false, markTaggedPagesAsPatrolled: true, tagArticleSortOrder: "cat", customTagList: [], // Stub watchStubbedPages: true, markStubbedPagesAsMinor: false, markStubbedPagesAsPatrolled: true, stubArticleSortOrder: "cat", // Welcome topWelcomes: false, watchWelcomes: true, welcomeHeading: "Welcome", insertHeadings: true, insertUsername: true, insertSignature: true, // sign welcome templates, where appropriate quickWelcomeMode: "norm", quickWelcomeTemplate: "welcome", customWelcomeList: [], // Talkback markTalkbackAsMinor: true, insertTalkbackSignature: true, // always sign talkback templates talkbackHeading: "Talkback", adminNoticeHeading: "Notice", mailHeading: "You've got mail!", // Shared markSharedIPAsMinor: true }; Twinkle.getPref = function twinkleGetPref( name ) { var result; if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.twinkle === "object" ) { // look in Twinkle.prefs (twinkleoptions.js) result = Twinkle.prefs.twinkle[name]; } else if ( typeof window.TwinkleConfig === "object" ) { // look in TwinkleConfig result = window.TwinkleConfig[name]; } if ( result === undefined ) { return Twinkle.defaultConfig.twinkle[name]; } return result; }; Twinkle.getFriendlyPref = function twinkleGetFriendlyPref(name) { var result; if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.friendly === "object" ) { // look in Twinkle.prefs (twinkleoptions.js) result = Twinkle.prefs.friendly[ name ]; } else if ( typeof window.FriendlyConfig === "object" ) { // look in FriendlyConfig result = window.FriendlyConfig[ name ]; } if ( result === undefined ) { return Twinkle.defaultConfig.friendly[ name ]; } return result; }; /** * **************** twAddPortlet() **************** * * Adds a portlet menu to one of the navigation areas on the page. * This is necessarily quite a hack since skins, navigation areas, and * portlet menu types all work slightly different. * * Available navigation areas depend on the skin used. * Vector: * For each option, the outer div class contains "vector-menu", the inner div class is "vector-menu-content", and the ul is "vector-menu-content-list" * "mw-panel", outer div class contains "vector-menu-portal". Existing portlets/elements: "p-logo", "p-navigation", "p-interaction", "p-tb", "p-coll-print_export" * "left-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-namespaces", "p-variants" (menu) * "right-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-views", "p-cactions" (menu), "p-search" * Special layout of p-personal portlet (part of "head") through specialized styles. * Monobook: * "column-one", outer div class "portlet", inner div class "pBody". Existing portlets: "p-cactions", "p-personal", "p-logo", "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export" * Special layout of p-cactions and p-personal through specialized styles. * Modern: * "mw_contentwrapper" (top nav), outer div class "portlet", inner div class "pBody". Existing portlets or elements: "p-cactions", "mw_content" * "mw_portlets" (sidebar), outer div class "portlet", inner div class "pBody". Existing portlets: "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export" * * @param String navigation -- id of the target navigation area (skin dependant, on vector either of "left-navigation", "right-navigation", or "mw-panel") * @param String id -- id of the portlet menu to create, preferably start with "p-". * @param String text -- name of the portlet menu to create. Visibility depends on the class used. * @param String type -- type of portlet. Currently only used for the vector non-sidebar portlets, pass "menu" to make this portlet a drop down menu. * @param Node nextnodeid -- the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end. * * @return Node -- the DOM node of the new item (a DIV element) or null */ function twAddPortlet( navigation, id, text, type, nextnodeid ) { //sanity checks, and get required DOM nodes var root = document.getElementById( navigation ); if ( !root ) { return null; } var item = document.getElementById( id ); if ( item ) { if ( item.parentNode && item.parentNode === root ) { return item; } return null; } var nextnode; if ( nextnodeid ) { nextnode = document.getElementById(nextnodeid); } if ((mw.config.get('skin') !== 'vector' && mw.config.get('skin') !== 'vector-2022') || (navigation !== 'left-navigation' && navigation !== 'right-navigation')) { type = null; // menu supported only in vector's #left-navigation & #right-navigation } var outerDivClass; var innerDivClass; switch (mw.config.get('skin')) { case "vector": case 'vector-2022': if ( navigation !== "portal" && navigation !== "left-navigation" && navigation !== "right-navigation" ) { navigation = "mw-panel"; } outerDivClass = 'vector-menu vector-menu-' + (navigation === 'mw-panel' ? 'portal' : type === 'menu' ? 'dropdown vector-menu-dropdown-noicon' : 'tabs'); innerDivClass = 'vector-menu-content'; break; case "modern": if ( navigation !== "mw_portlets" && navigation !== "mw_contentwrapper" ) { navigation = "mw_portlets"; } outerDivClass = "portlet"; innerDivClass = "pBody"; break; default: navigation = "column-one"; outerDivClass = "portlet"; innerDivClass = "pBody"; break; } // Build the DOM elements. var outerDiv = document.createElement('nav'); outerDiv.setAttribute('aria-labelledby', id + '-label'); // Vector getting vector-menu-empty FIXME TODO outerDiv.className = outerDivClass + ' emptyPortlet'; outerDiv.id = id; if (nextnode && nextnode.parentNode === root) { root.insertBefore(outerDiv, nextnode); } else { root.appendChild(outerDiv); } var h3 = document.createElement('h3'); h3.id = id + '-label'; var ul = document.createElement('ul'); if (mw.config.get( "skin" ) === 'vector' || mw.config.get("skin") === 'vector-2022') { h3.className = "vector-menu-heading"; // add invisible checkbox to keep menu open when clicked // similar to the p-cactions ("More") menu if (outerDivClass.indexOf('vector-menu-dropdown') !== -1) { var chkbox = document.createElement('input'); chkbox.className = 'vectorMenuCheckbox vector-menu-checkbox'; // remove vectorMenuCheckbox after 1.35-wmf.37 goes live chkbox.setAttribute('type', 'checkbox'); chkbox.setAttribute('aria-labelledby', id + '-label'); outerDiv.appendChild(chkbox); var span = document.createElement('span'); span.appendChild(document.createTextNode(text)); h3.appendChild(span); var a = document.createElement('a'); a.href = '#'; $(a).click(function(e) { e.preventDefault(); }); h3.appendChild(a); } outerDiv.appendChild(h3); ul.className = 'menu vector-menu-content-list'; // remove menu after 1.35-wmf.37 goes live } else { h3.appendChild(document.createTextNode(text)); outerDiv.appendChild(h3); } if (innerDivClass) { var innerDiv = document.createElement('div'); innerDiv.className = innerDivClass; innerDiv.appendChild(ul); outerDiv.appendChild(innerDiv); } else { outerDiv.appendChild(ul); } return outerDiv; } /** * **************** twAddPortletLink() **************** * Builds a portlet menu if it doesn't exist yet, and add the portlet link. * @param task: Either a URL for the portlet link or a function to execute. */ function twAddPortletLink( task, text, id, tooltip ) { if ( Twinkle.getPref("portletArea") !== null ) { twAddPortlet( Twinkle.getPref( "portletArea" ), Twinkle.getPref( "portletId" ), Twinkle.getPref( "portletName" ), Twinkle.getPref( "portletType" ), Twinkle.getPref( "portletNext" )); } var link = mw.util.addPortletLink( Twinkle.getPref( "portletId" ), typeof task === "string" ? task : "#", text, id, tooltip ); $('.client-js .skin-vector #p-cactions').css('margin-right', 'initial'); if ( $.isFunction( task ) ) { $( link ).click(function ( ev ) { task(); ev.preventDefault(); }); } if ($.collapsibleTabs) { $.collapsibleTabs.handleResize(); } return link; } // Check if account is experienced enough to use Twinkle var twinkleUserAuthorized = Morebits.userIsInGroup( "autoconfirmed" ) || Morebits.userIsInGroup( "confirmed" ) || Morebits.userIsInGroup( "sysop" ); /* **************************************** *** friendlyshared.js: Shared IP tagging module **************************************** * Mode of invocation: Tab ("Shared") * Active on: Existing IP user talk pages * Config directives in: FriendlyConfig */ Twinkle.shared = function friendlyshared() { if( mw.config.get('wgNamespaceNumber') === 3 && Morebits.isIPAddress(mw.config.get('wgTitle')) ) { var username = mw.config.get('wgTitle').split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes twAddPortletLink( function(){ Twinkle.shared.callback(username); }, "Shared IP", "friendly-shared", "Shared IP tagging" ); } }; Twinkle.shared.callback = function friendlysharedCallback( uid ) { var Window = new Morebits.simpleWindow( 600, 400 ); Window.setTitle( "Shared IP address tagging" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#shared" ); var form = new Morebits.quickForm( Twinkle.shared.callback.evaluate ); var div = form.append( { type: 'div', id: 'sharedip-templatelist' } ); div.append( { type: 'header', label: 'Shared IP address templates' } ); div.append( { type: 'radio', name: 'shared', list: Twinkle.shared.standardList, event: function( e ) { Twinkle.shared.callback.change_shared( e ); e.stopPropagation(); } } ); var org = form.append( { type:'field', label:'Fill in other details (optional) and click \"Submit\"' } ); org.append( { type: 'input', name: 'organization', label: 'IP address owner/operator', disabled: true, tooltip: 'You can optionally enter the name of the organization that owns/operates the IP address. You can use wikimarkup if necessary.' } ); org.append( { type: 'input', name: 'host', label: 'Host name (optional)', disabled: true, tooltip: 'The host name (for example, proxy.example.com) can be optionally entered here and will be linked by the template.' } ); org.append( { type: 'input', name: 'contact', label: 'Contact information (only if requested)', disabled: true, tooltip: 'You can optionally enter some contact details for the organization. Use this parameter only if the organization has specifically requested that it be added. You can use wikimarkup if necessary.' } ); form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); $(result).find('div#sharedip-templatelist').addClass('quickform-scrollbox'); }; Twinkle.shared.standardList = [ { label: '{{SharedIP}}: standard shared IP address template', value: 'Shared IP', tooltip: 'IP user talk page template that shows helpful information to IP users and those wishing to warn, block or ban them' }, { label: '{{SchoolIP}}: shared IP address template modified for educational institutions', value: 'SchoolIP' }, { label: '{{SharedIPCORP}}: shared IP address template modified for businesses', value: 'SharedIPCORP' }, { label: '{{ISP}}: shared IP address template modified for ISP organizations (specifically proxies)', value: 'ISP' } ]; Twinkle.shared.callback.change_shared = function friendlysharedCallbackChangeShared(e) { if( e.target.value === 'Shared IP edu' ) { e.target.form.contact.disabled = false; } else { e.target.form.contact.disabled = true; } e.target.form.organization.disabled=false; e.target.form.host.disabled=false; }; Twinkle.shared.callbacks = { main: function( pageobj ) { var params = pageobj.getCallbackParameters(); var pageText = pageobj.getPageText(); var found = false; var text = '{{'; for( var i=0; i < Twinkle.shared.standardList.length; i++ ) { var tagRe = new RegExp( '(\\{\\{' + Twinkle.shared.standardList[i].value + '(\\||\\}\\}))', 'im' ); if( tagRe.exec( pageText ) ) { Morebits.status.warn( 'Info', 'Found {{' + Twinkle.shared.standardList[i].value + '}} on the user\'s talk page already...aborting' ); found = true; } } if( found ) { return; } Morebits.status.info( 'Info', 'Will add the shared IP address template to the top of the user\'s talk page.' ); text += params.value + '|' + params.organization; if( params.value === 'shared IP edu' && params.contact !== '') { text += '|' + params.contact; } if( params.host !== '' ) { text += '|host=' + params.host; } text += '}}\n\n'; var summaryText = 'Added {{[[Template:' + params.value + '|' + params.value + ']]}} template.'; pageobj.setPageText(text + pageText); pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markSharedIPAsMinor')); pageobj.setCreateOption('recreate'); pageobj.save(); } }; Twinkle.shared.callback.evaluate = function friendlysharedCallbackEvaluate(e) { var shared = e.target.getChecked( 'shared' ); if( !shared || shared.length <= 0 ) { alert( 'You must select a shared IP address template to use!' ); return; } var value = shared[0]; if( e.target.organization.value === '') { alert( 'You must input an organization for the {{' + value + '}} template!' ); return; } var params = { value: value, organization: e.target.organization.value, host: e.target.host.value, contact: e.target.contact.value }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.shared.callbacks.main); }; /* **************************************** *** friendlytag.js: Tag module **************************************** * Mode of invocation: Tab ("Tag") * Active on: Existing articles; file pages with a corresponding file * which is local (not on Commons); existing subpages of * {Wikipedia|Wikipedia talk}:Articles for creation; * all redirects * Config directives in: FriendlyConfig */ Twinkle.tag = function friendlytag() { // redirect tagging if( Morebits.wiki.isPageRedirect() ) { Twinkle.tag.mode = 'redirect'; //twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Tag redirect" ); } // file tagging else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) { Twinkle.tag.mode = 'file'; } // article/draft article tagging else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) { Twinkle.tag.mode = 'article'; //twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Add maintenance tags to article" ); } }; Twinkle.tag.callback = function friendlytagCallback( uid ) { var Window = new Morebits.simpleWindow( 630, (Twinkle.tag.mode === "article") ? 450 : 400 ); Window.setScriptName( "Twinkle" ); // anyone got a good policy/guideline/info page/instructional page link?? Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#tag" ); var form = new Morebits.quickForm( Twinkle.tag.callback.evaluate ); switch( Twinkle.tag.mode ) { case 'article': Window.setTitle( "Article maintenance tagging" ); form.append( { type: 'checkbox', list: [ { label: 'Group inside {{multiple issues}} if possible', value: 'group', name: 'group', tooltip: 'If applying three or more templates supported by {{multiple issues}} and this box is checked, all supported templates will be grouped inside a {{multiple issues}} template.', checked: Twinkle.getFriendlyPref('groupByDefault') } ] } ); form.append({ type: 'select', name: 'sortorder', label: 'View this list:', tooltip: 'You can change the default view order in your Twinkle preferences (mh:dev:Twinkle/Preferences).', event: Twinkle.tag.updateSortOrder, list: [ { type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'cat' }, { type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'alpha' } ] }); form.append( { type: 'div', id: 'tagWorkArea' } ); if( Twinkle.getFriendlyPref('customTagList').length ) { form.append( { type: 'header', label: 'Custom tags' } ); form.append( { type: 'checkbox', name: 'articleTags', list: Twinkle.getFriendlyPref('customTagList') } ); } break; case 'redirect': Window.setTitle( "Redirect tagging" ); //Spelling, misspelling, tense and capitalization templates form.append({ type: 'header', label:'All templates' }); form.append({ type: 'checkbox', name: 'redirectTags', list: Twinkle.tag.spellingList }); break; default: alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode); break; } form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); if (Twinkle.tag.mode === "article") { // fake a change event on the sort dropdown, to initialize the tag list var evt = document.createEvent("Event"); evt.initEvent("change", true, true); result.sortorder.dispatchEvent(evt); } }; Twinkle.tag.checkedTags = []; Twinkle.tag.updateSortOrder = function(e) { var sortorder = e.target.value; var $workarea = $(e.target.form).find("div#tagWorkArea"); Twinkle.tag.checkedTags = e.target.form.getChecked("articleTags"); if (!Twinkle.tag.checkedTags) { Twinkle.tag.checkedTags = []; } // function to generate a checkbox, with appropriate subgroup if needed var makeCheckbox = function(tag, description) { var checkbox = { value: tag, label: "{{" + tag + "}}: " + description }; if (Twinkle.tag.checkedTags.indexOf(tag) !== -1) { checkbox.checked = true; } if (tag === "notability") { checkbox.subgroup = { name: 'notability', type: 'select', list: [ { label: "{{notability}}: article\'s subject may not meet the general notability guideline", value: "none" }, { label: "{{notability|Academics}}: notability guideline for academics", value: "Academics" }, { label: "{{notability|Biographies}}: notability guideline for biographies", value: "Biographies" }, { label: "{{notability|Books}}: notability guideline for books", value: "Books" }, { label: "{{notability|Companies}}: notability guidelines for companies and organizations", value: "Companies" }, { label: "{{notability|Events}}: notability guideline for events", value: "Events" }, { label: "{{notability|Films}}: notability guideline for films", value: "Films" }, { label: "{{notability|Music}}: notability guideline for music", value: "Music" }, { label: "{{notability|Neologisms}}: notability guideline for neologisms", value: "Neologisms" }, { label: "{{notability|Numbers}}: notability guideline for numbers", value: "Numbers" }, { label: "{{notability|Products}}: notability guideline for products and services", value: "Products" }, { label: "{{notability|Sport}}: notability guideline for sports and athletics", value: "Sport" }, { label: "{{notability|Web}}: notability guideline for web content", value: "Web" } ] }; } return checkbox; }; // categorical sort order if (sortorder === "cat") { var div = new Morebits.quickForm.element({ type: "div", id: "tagWorkArea" }); // function to iterate through the tags and create a checkbox for each one var doCategoryCheckboxes = function(subdiv, array) { var checkboxes = []; $.each(array, function(k, tag) { var description = Twinkle.tag.article.tags[tag]; checkboxes.push(makeCheckbox(tag, description)); }); subdiv.append({ type: "checkbox", name: "articleTags", list: checkboxes }); }; var i = 0; // go through each category and sub-category and append lists of checkboxes $.each(Twinkle.tag.article.tagCategories, function(title, content) { div.append({ type: "header", id: "tagHeader" + i, label: title }); var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ }); if ($.isArray(content)) { doCategoryCheckboxes(subdiv, content); } else { $.each(content, function(subtitle, subcontent) { subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] }); doCategoryCheckboxes(subdiv, subcontent); }); } }); var rendered = div.render(); $workarea.replaceWith(rendered); var $rendered = $(rendered); $rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' }); $rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' }); } // alphabetical sort order else { var checkboxes = []; $.each(Twinkle.tag.article.tags, function(tag, description) { checkboxes.push(makeCheckbox(tag, description)); }); var tags = new Morebits.quickForm.element({ type: "checkbox", name: "articleTags", list: checkboxes }); $workarea.empty().append(tags.render()); } }; // Tags for ARTICLES start here Twinkle.tag.article = {}; // A list of all article tags, in alphabetical order // To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below. Twinkle.tag.article.tags = { "advertisement": "article is written like an advertisement", "autobiography": "article is an autobiography and may not be written neutrally", "BLP sources": "BLP article needs more sources for verification", "BLP unsourced": "BLP article has no sources at all", "citation style": "article has unclear or inconsistent inline citations", "cleanup": "article may require cleanup", "COI": "article creator or major contributor may have a conflict of interest", "complex": "the English used in this article or section may not be easy for everybody to understand", "confusing": "article may be confusing or unclear", "context": "article provides insufficient context", "copyedit": "article needs copy editing for grammar, style, cohesion, tone, and/or spelling", "dead end": "article has few or no links to other articles", "disputed": "article has questionable factual accuracy", "expert-subject": "article needs attention from an expert on the subject", "external links": "article's external links may not follow content policies or guidelines", "fansite": "article resembles a fansite", "fiction": "article fails to distinguish between fact and fiction", "globalise": "article may not represent a worldwide view of the subject", "hoax": "article may be a complete hoax", "in-universe": "article subject is fictional and needs rewriting from a non-fictional perspective", "in use": "article is undergoing a major edit for a short while", "intro-missing": "article has no lead section and one should be written", "intro-rewrite": "article lead section needs to be rewritten", "intro-tooshort": "article lead section is too short and should be expanded", "jargon": "article uses technical words that not everybody will know", "link rot": "article uses bare URLs for references, which are prone to link rot", "merge": "article should be merged with another given article", "metricate": "article exclusively uses non-SI units of measurement", "more footnotes": "article has some references, but insufficient in-text citations", "more sources": "article needs more sources for verification", "no footnotes": "article has references, but no in-text citations", "no sources": "article has no references at all", "notability": "article's subject may not meet the notability guideline", "NPOV": "article does not maintain a neutral point of view", "one source": "article relies largely or entirely upon a single source", "original research": "article has original research or unverified claims", "orphan": "article is linked to from no other articles", "plot": "plot summary in article is too long", "primary sources": "article relies too heavily on first-hand sources, and needs third-party sources", "prose": "article is in a list format that may be better presented using prose", "redlinks": "article may have too many red links", "restructure": "article may be in need of reorganization to comply with Wikipedia's layout guidelines", "rough translation": "article is poorly translated and needs cleanup", "sections": "article needs to be broken into sections", "self-published": "article may contain improper references to self-published sources", "tone": "tone of article is not appropriate", "uncat": "article is uncategorized", "under construction": "article is currently in the middle of an expansion or major revamping", "unreliable sources": "article's references may not be reliable sources", "update": "article needs additional up-to-date information added", "very long": "article is too long", "weasel": "article neutrality is compromised by the use of weasel words", "wikify": "article needs to be wikified" }; // A list of tags in order of category // Tags should be in alphabetical order within the categories // Add new categories with discretion - the list is long enough as is! Twinkle.tag.article.tagCategories = { "Cleanup and maintenance tags": { "General maintenance tags": [ "cleanup", "complex", "copyedit", "wikify" ], "Potentially unwanted content": [ "external links" ], "Structure, formatting, and lead section": [ "intro-missing", "intro-rewrite", "intro-tooshort", "restructure", "sections", "very long" ], "Fiction-related cleanup": [ "fiction", "in-universe", "plot" ] }, "General content issues": { "Importance and notability": [ "notability" // has subcategories and special-cased code ], "Style of writing": [ "advertisement", "fansite", "jargon", "prose", "redlinks", "tone" ], "Sense (or lack thereof)": [ "confusing" ], "Information and detail": [ "context", "expert-subject", "metricate" ], "Timeliness": [ "update" ], "Neutrality, bias, and factual accuracy": [ "autobiography", "COI", "disputed", "hoax", "globalise", "NPOV", "weasel" ], "Verifiability and sources": [ "BLP sources", "BLP unsourced", "more sources", "no sources", "one source", "original research", "primary sources", "self-published", "unreliable sources" ] }, "Specific content issues": { "Language": [ "complex" ], "Links": [ "dead end", "orphan", "wikify" // this tag is listed twice because it used to focus mainly on links, but now it's a more general cleanup tag ], "Referencing technique": [ "citation style", "link rot", "more footnotes", "no footnotes" ], "Categories": [ "uncat" ] }, "Merging": [ "merge", ], "Informational": [ "in use", "under construction" ] }; // Tags for REDIRECTS start here Twinkle.tag.spellingList = [ { label: '{{R from capitalization}}: redirect from a from a capitalized title', value: 'R from capitalization' }, { label: '{{R with other capitalizations}}: redirect from a title with a different capitalization', value: 'R with other capitalizations' }, { label: '{{R from other name}}: redirect from a title with a different name', value: 'R from other name' }, { label: '{{R from other spelling}}: redirect from a title with a different spelling', value: 'R from other spelling' }, { label: '{{R from plural}}: redirect from a plural title', value: 'R from plural' }, { label: '{{R from related things}}: redirect related title', value: 'R from related things' }, { label: '{{R to section}}: redirect from a title for a "minor topic or title" to a comprehensive-type article section which covers the subject', value: 'R to section' }, { label: '{{R from shortcut}}: redirect to a Wikipedia "shortcut"', value: 'R from shortcut' }, { label: '{{R from title without diacritics}}: redirect to the article title with diacritical marks (accents, umlauts, etc.)', value: 'R from title without diacritics' } ]; // Contains those article tags that *do not* work inside {{multiple issues}}. Twinkle.tag.multipleIssuesExceptions = [ 'cat improve', 'in use', 'merge', 'merge from', 'merge to', 'not English', 'rough translation', 'uncat', 'under construction', ]; Twinkle.tag.callbacks = { main: function( pageobj ) { var params = pageobj.getCallbackParameters(), tagRe, tagText = '', summaryText = 'Added', tags = [], groupableTags = [], i, totalTags var pageText = pageobj.getPageText(); var addTag = function friendlytagAddTag( tagIndex, tagName ) { var currentTag = ""; if( tagName === 'globalize' ) { currentTag += '{{' + params.globalizeSubcategory; } else { currentTag += ( Twinkle.tag.mode === 'redirect' ? '\n' : '' ) + '{{' + tagName; } if( tagName === 'notability' && params.notabilitySubcategory !== 'none' ) { currentTag += '|' + params.notabilitySubcategory; } // prompt for other parameters, based on the tag switch( tagName ) { case 'cleanup': var reason = prompt('"The specific problem is: " \n' + "This information is optional. Just click OK if you don't wish to enter this.", ""); if (reason === null) { Morebits.status.warn("Notice", "{{cleanup}} tag skipped by user"); return true; // continue to next tag } else { currentTag += '|reason=' + reason; } break; case 'complex': var cpreason = prompt('"An editor’s reason for this is:" (e.g. "words like XX") \n' + "Just click OK if you don't wish to enter this. To skip the {{complex}} tag, click Cancel.", ""); if (cpreason === null) { return true; // continue to next tag } else if (cpreason !== "") { currentTag += '|2=' + cpreason; } break; case 'copyedit': var cereason = prompt('"This article may require copy editing for..." (e.g. "consistent spelling") \n' + "Just click OK if you don't wish to enter this. To skip the {{copyedit}} tag, click Cancel.", ""); if (cereason === null) { return true; // continue to next tag } else if (cereason !== "") { currentTag += '|for=' + cereason; } break; case 'expert-subject': var esreason = prompt('"This is because..." \n' + "This information is optional. To skip the {{expert-subject}} tag, click Cancel.", ""); if (esreason === null) { return true; // continue to next tag } else if (esreason !== "") { currentTag += '|1=' + esreason; } break; case 'not English': var langname = prompt('Please enter the name of the language the article is thought to be written in. \n' + "Just click OK if you don't know. To skip the {{not English}} tag, click Cancel.", ""); if (langname === null) { return true; // continue to next tag } else if (langname !== "") { currentTag += '|1=' + langname; } break; case 'rough translation': var roughlang = prompt('Please enter the name of the language the article is thought to have been translated from. \n' + "Just click OK if you don't know. To skip the {{rough translation}} tag, click Cancel.", ""); if (roughlang === null) { return true; // continue to next tag } else if (roughlang !== "") { currentTag += '|1=' + roughlang; } break; case 'wikify': var wreason = prompt('You can optionally enter a more specific reason why the article needs to be wikified: This article needs to be wikified. {{{Your reason here}}} \n' + "Just click OK if you don't wish to enter this. To skip the {{wikify}} tag, click Cancel.", ""); if (wreason === null) { return true; // continue to next tag } else if (wreason !== "") { currentTag += '|reason=' + wreason; } break; case 'merge': case 'merge to': case 'merge from': var param = prompt('Please enter the name of the other article(s) involved in the merge. \n' + "To specify multiple articles, separate them with a vertical pipe (|) character. \n" + "This information is required. Click OK when done, or click Cancel to skip the merge tag.", ""); if (param === null) { return true; // continue to next tag } else if (param !== "") { currentTag += '|' + param; } break; default: break; } currentTag += (Twinkle.tag.mode === 'redirect') ? '}}' : '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n'; tagText += currentTag; if ( tagIndex > 0 ) { if( tagIndex === (totalTags - 1) ) { summaryText += ' and'; } else if ( tagIndex < (totalTags - 1) ) { summaryText += ','; } } summaryText += ' {{[['; summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName)); summaryText += ']]}}'; }; if( Twinkle.tag.mode !== 'redirect' ) { // Check for preexisting tags and separate tags into groupable and non-groupable arrays for( i = 0; i < params.tags.length; i++ ) { tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' ); if( !tagRe.exec( pageText ) ) { if( Twinkle.tag.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) { groupableTags = groupableTags.concat( params.tags[i] ); } else { tags = tags.concat( params.tags[i] ); } } else { Morebits.status.info( 'Info', 'Found {{' + params.tags[i] + '}} on the article already...excluding' ); } } if( params.group && groupableTags.length >= 3 ) { Morebits.status.info( 'Info', 'Grouping supported tags inside {{multiple issues}}' ); groupableTags.sort(); tagText += '{{multiple issues|\n'; totalTags = groupableTags.length; $.each(groupableTags, addTag); summaryText += ' tags (within {{[[Template:multiple issues|multiple issues]]}})'; if( tags.length > 0 ) { summaryText += ', and'; } tagText += '}}\n'; } else { tags = tags.concat( groupableTags ); } } else { // Redirect tagging: Check for pre-existing tags for( i = 0; i < params.tags.length; i++ ) { tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' ); if( !tagRe.exec( pageText ) ) { tags = tags.concat( params.tags[i] ); } else { Morebits.status.info( 'Info', 'Found {{' + params.tags[i] + '}} on the redirect already...excluding' ); } } } tags.sort(); totalTags = tags.length; $.each(tags, addTag); if( Twinkle.tag.mode === 'redirect' ) { pageText += tagText; } else { // smartly insert the new tags after any hatnotes. Regex is a bit more // complicated than it'd need to be, to allow templates as parameters, // and to handle whitespace properly. pageText = pageText.replace(/^\s*(?:((?:\s*\{\{\s*(?:about|correct title|dablink|distinguish|for|other\s?(?:hurricaneuses|people|persons|places|uses(?:of)?)|redirect(?:-acronym)?|see\s?(?:also|wiktionary)|selfref|the)\d*\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\})+(?:\s*\n)?)\s*)?/i, "$1" + tagText); } summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) + ' to ' + Twinkle.tag.mode + Twinkle.getPref('summaryAd'); pageobj.setPageText(pageText); pageobj.setEditSummary(summaryText); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) { pageobj.patrol(); } }, file: function friendlytagCallbacksFile(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var summary = "Adding "; // Add maintenance tags if (params.tags.length) { var tagtext = "", currentTag; $.each(params.tags, function(k, tag) { currentTag += "}}\n"; tagtext += currentTag; summary += "{{" + tag + "}}, "; return true; // continue }); if (!tagtext) { pageobj.getStatusElement().warn("User canceled operation; nothing to do"); return; } text = tagtext + text; } pageobj.setPageText(text); pageobj.setEditSummary(summary.substring(0, summary.length - 2) + Twinkle.getPref('summaryAd')); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) { pageobj.patrol(); } } }; Twinkle.tag.callback.evaluate = function friendlytagCallbackEvaluate(e) { var form = e.target; var params = {}; switch (Twinkle.tag.mode) { case 'article': params.tags = form.getChecked( 'articleTags' ); params.group = form.group.checked; params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null; break; case 'file': params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null; params.tags = form.getChecked( 'imageTags' ); break; case 'redirect': params.tags = form.getChecked( 'redirectTags' ); break; default: alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode); break; } if( !params.tags.length ) { alert( 'You must select at least one tag!' ); return; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds"; if (Twinkle.tag.mode === 'redirect') { Morebits.wiki.actionCompleted.followRedirect = false; } var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.tag.mode); wikipedia_page.setCallbackParameters(params); switch (Twinkle.tag.mode) { case 'article': /* falls through */ case 'redirect': wikipedia_page.load(Twinkle.tag.callbacks.main); return; case 'file': wikipedia_page.load(Twinkle.tag.callbacks.file); return; default: alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode); break; } }; /* **************************************** *** twinklestub.js: Tag module **************************************** * Mode of invocation: Tab ("Stub") * Active on: Existing articles * Config directives in: FriendlyConfig * Note: customised friendlytag module (for SEWP) */ Twinkle.stub = function friendlytag() { // redirect tagging if( Morebits.wiki.isPageRedirect() ) { Twinkle.stub.mode = 'redirect'; } // file tagging else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) { Twinkle.stub.mode = 'file'; } // article/draft article tagging else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) { Twinkle.stub.mode = 'article'; //twAddPortletLink( Twinkle.stub.callback, "Stub", "friendly-tag", "Add stub tags to article" ); } }; Twinkle.stub.callback = function friendlytagCallback( uid ) { var Window = new Morebits.simpleWindow( 630, (Twinkle.stub.mode === "article") ? 450 : 400 ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Simple Stub project", "Wikipedia:Simple Stub Project" ); Window.addFooterLink( "Stub guideline", "Wikipedia:Stub" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#stub" ); var form = new Morebits.quickForm( Twinkle.stub.callback.evaluate ); switch( Twinkle.stub.mode ) { case 'article': Window.setTitle( "Article stub tagging" ); form.append({ type: 'select', name: 'sortorder', label: 'View this list:', tooltip: 'You can change the default view order in your Twinkle preferences (https://dev.miraheze.org/wiki/Twinkle/Preferences).', event: Twinkle.stub.updateSortOrder, list: [ { type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'cat' }, { type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'alpha' } ] }); form.append( { type: 'div', id: 'tagWorkArea' } ); } form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); if (Twinkle.stub.mode === "article") { // fake a change event on the sort dropdown, to initialize the tag list var evt = document.createEvent("Event"); evt.initEvent("change", true, true); result.sortorder.dispatchEvent(evt); } }; Twinkle.stub.checkedTags = []; Twinkle.stub.updateSortOrder = function(e) { var sortorder = e.target.value; var $workarea = $(e.target.form).find("div#tagWorkArea"); Twinkle.stub.checkedTags = e.target.form.getChecked("articleTags"); if (!Twinkle.stub.checkedTags) { Twinkle.stub.checkedTags = []; } // function to generate a checkbox, with appropriate subgroup if needed var makeCheckbox = function(tag, description) { var checkbox = { value: tag, label: "{{" + tag + "}}: " + description }; if (Twinkle.stub.checkedTags.indexOf(tag) !== -1) { checkbox.checked = true; } return checkbox; }; // categorical sort order if (sortorder === "cat") { var div = new Morebits.quickForm.element({ type: "div", id: "tagWorkArea" }); // function to iterate through the tags and create a checkbox for each one var doCategoryCheckboxes = function(subdiv, array) { var checkboxes = []; $.each(array, function(k, tag) { var description = Twinkle.stub.article.tags[tag]; checkboxes.push(makeCheckbox(tag, description)); }); subdiv.append({ type: "checkbox", name: "articleTags", list: checkboxes }); }; var i = 0; // go through each category and sub-category and append lists of checkboxes $.each(Twinkle.stub.article.tagCategories, function(title, content) { div.append({ type: "header", id: "tagHeader" + i, label: title }); var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ }); if ($.isArray(content)) { doCategoryCheckboxes(subdiv, content); } else { $.each(content, function(subtitle, subcontent) { subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] }); doCategoryCheckboxes(subdiv, subcontent); }); } }); var rendered = div.render(); $workarea.replaceWith(rendered); var $rendered = $(rendered); $rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' }); $rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' }); } // alphabetical sort order else { var checkboxes = []; $.each(Twinkle.stub.article.tags, function(tag, description) { checkboxes.push(makeCheckbox(tag, description)); }); var tags = new Morebits.quickForm.element({ type: "checkbox", name: "articleTags", list: checkboxes }); $workarea.empty().append(tags.render()); } }; // Tags for ARTICLES start here Twinkle.stub.article = {}; // A list of all article tags, in alphabetical order // To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below. Twinkle.stub.article.tags = { "actor-stub": "for use with articles about actors", "asia-stub": "for use with anything about Asia, except people", "bio-stub": "for use with all people, no matter who or what profession", "biology-stub": "for use with topics related to biology", "chem-stub": "for use with topics related to chemistry", "europe-stub": "for use with anything about Europe, except people", "france-geo-stub": "for use with France geography topics", "food-stub": "for use with anything about food", "geo-stub": "for use with all geographical locations (places, towns, cities, etc)", "history-stub": "for use with history topics", "japan-stub": "for use with anything about Japan, except people", "japan-sports-bio-stub": "for use with Japanese sport biographies", "list-stub": "for use with lists only", "lit-stub": "for use with all literature articles except people", "math-stub": "for use with topics related to mathematics", "med-stub": "for use with topics related to medicine", "military-stub": "for use with military related topics", "movie-stub": "for use with all movie articles except people", "music-stub": "for use with all music articles except people", "north-America-stub": "for use with anything about North America, except people", "performing-arts-stub": "general stub for the performing arts", "physics-stub": "for use with topics related to physics", "politics-stub": "for use with politics related topics", "religion-stub": "for use with religion related topics", "sci-stub": "anything science related (all branches and their tools)", "sport-stub": "general stub for all sports and sports items, not people", "sports-bio-stub": "for use with people who have sport as profession", "stub": "for all stubs that can not fit into any stub we have", "switzerland-stub": "for use with everything about Switzerland, except people", "tech-stub": "for use with technology related articles", "transport-stub": "for use with articles about any moving object (cars, bikes, ships, crafts, planes, rail, buses, trains, etc)", "tv-stub": "for use with all television articles except people", "UK-stub": "for use with anything about the United Kingdom, except people", "US-actor-stub": "for use with United States actor biographies", "US-bio-stub": "for use with United States biographies", "US-geo-stub": "for use with United States geography topics", "US-stub": "for use with anything about the United States, except people and geography", "video-game-stub": "for use with stubs related to video games", "weather-stub": "for articles about weather" }; // A list of tags in order of category // Tags should be in alphabetical order within the categories // Add new categories with discretion - the list is long enough as is! Twinkle.stub.article.tagCategories = { "Stub templates": [ "stub", "list-stub" ], "Countries & Geography": [ "asia-stub", "europe-stub", "france-geo-stub", "geo-stub", "japan-stub", "japan-sports-bio-stub", "north-America-stub", "switzerland-stub", "UK-stub", "US-bio-stub", "US-geo-stub", "US-stub" ], "Miscellaneous": [ "food-stub", "history-stub", "military-stub", "politics-stub", "religion-stub", "transport-stub" ], "People": [ "actor-stub", "bio-stub", "japan-sports-bio-stub", "sports-bio-stub", "US-actor-stub", "US-bio-stub" ], "Science": [ "biology-stub", "chem-stub", "math-stub", "med-stub", "physics-stub", "sci-stub", "weather-stub" ], "Sports": [ "japan-sports-bio-stub", "sport-stub", "sports-bio-stub" ], "Technology": [ "tech-stub", "video-game-stub" ], "Arts": [ "actor-stub", "lit-stub", "movie-stub", "music-stub", "performing-arts-stub", "tv-stub", "US-actor-stub" ] } // Tags for REDIRECTS start here // Contains those article tags that *do not* work inside {{multiple issues}}. Twinkle.stub.multipleIssuesExceptions = [ 'cat improve', 'in use', 'merge', 'merge from', 'merge to', 'not English', 'rough translation', 'uncat', 'under construction', 'update' ]; Twinkle.stub.callbacks = { main: function( pageobj ) { var params = pageobj.getCallbackParameters(), tagRe, tagText = '', summaryText = 'Added', tags = [], groupableTags = [], i, totalTags; // Remove tags that become superfluous with this action var pageText = pageobj.getPageText(); var addTag = function friendlytagAddTag( tagIndex, tagName ) { var currentTag = ""; pageText += '\n\n{{' + tagName + '}}'; if ( tagIndex > 0 ) { if( tagIndex === (totalTags - 1) ) { summaryText += ' and'; } else if ( tagIndex < (totalTags - 1) ) { summaryText += ','; } } summaryText += ' {{[['; summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName)); summaryText += ']]}}'; }; // Check for preexisting tags and separate tags into groupable and non-groupable arrays for( i = 0; i < params.tags.length; i++ ) { tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' ); if( !tagRe.exec( pageText ) ) { if( Twinkle.stub.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) { groupableTags = groupableTags.concat( params.tags[i] ); } else { tags = tags.concat( params.tags[i] ); } } else { Morebits.status.info( 'Info', 'Found {{' + params.tags[i] + '}} on the article already...excluding' ); } } tags = tags.concat( groupableTags ); tags.sort(); totalTags = tags.length; $.each(tags, addTag); summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) + ' to ' + Twinkle.stub.mode + Twinkle.getPref('summaryAd'); pageobj.setPageText(pageText); pageobj.setEditSummary(summaryText); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchStubbedPages')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markStubbedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if( Twinkle.getFriendlyPref('markStubbedPagesAsPatrolled') ) { pageobj.patrol(); } } }; Twinkle.stub.callback.evaluate = function friendlytagCallbackEvaluate(e) { var form = e.target; var params = {}; switch (Twinkle.stub.mode) { case 'article': params.tags = form.getChecked( 'articleTags' ); params.group = false; params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null; break; case 'file': params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null; params.tags = form.getChecked( 'imageTags' ); break; case 'redirect': params.tags = form.getChecked( 'redirectTags' ); break; } if( !params.tags.length ) { alert( 'You must select at least one tag!' ); return; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds"; if (Twinkle.stub.mode === 'redirect') { Morebits.wiki.actionCompleted.followRedirect = false; } var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.stub.mode); wikipedia_page.setCallbackParameters(params); switch (Twinkle.stub.mode) { case 'article': /* falls through */ case 'redirect': wikipedia_page.load(Twinkle.stub.callbacks.main); return; case 'file': wikipedia_page.load(Twinkle.stub.callbacks.file); return; } }; /* **************************************** *** friendlytalkback.js: Talkback module **************************************** * Mode of invocation: Tab ("TB") * Active on: Existing user talk pages * Config directives in: FriendlyConfig */ ;(function(){ Twinkle.talkback = function() { if ( Morebits.getPageAssociatedUser() === false ) { return; } twAddPortletLink( callback, "TB", "friendly-talkback", "Easy talkback" ); }; var callback = function( ) { if( Morebits.getPageAssociatedUser() === mw.config.get("wgUserName") && !confirm("Is it really so bad that you're talking back to yourself?") ){ return; } var Window = new Morebits.simpleWindow( 600, 350 ); Window.setTitle("Talkback"); Window.setScriptName("Twinkle"); Window.addFooterLink( "About {{talkback}}", "Template:Talkback" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#talkback" ); var form = new Morebits.quickForm( callback_evaluate ); form.append({ type: "radio", name: "tbtarget", list: [ { label: "Talkback: my talk page", value: "mytalk", checked: "true" }, { label: "Talkback: other user talk page", value: "usertalk" }, { label: "Talkback: other page", value: "other" }, { label: "Noticeboard notification", value: "notice" }, { label: "\"You've got mail\"", value: "mail" }, { label: "Whisperback", value: "wb" } ], event: callback_change_target }); form.append({ type: "field", label: "Work area", name: "work_area" }); form.append({ type: "submit" }); var result = form.render(); Window.setContent( result ); Window.display(); // We must init the var evt = document.createEvent("Event"); evt.initEvent( "change", true, true ); result.tbtarget[0].dispatchEvent( evt ); }; var prev_page = ""; var prev_section = ""; var prev_message = ""; var callback_change_target = function( e ) { var value = e.target.values; var root = e.target.form; var old_area = Morebits.quickForm.getElements(root, "work_area")[0]; if(root.section) { prev_section = root.section.value; } if(root.message) { prev_message = root.message.value; } if(root.page) { prev_page = root.page.value; } var work_area = new Morebits.quickForm.element({ type: "field", label: "Talkback information", name: "work_area" }); switch( value ) { case "mytalk": /* falls through */ default: work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on your talk page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; case "usertalk": work_area.append({ type:"input", name:"page", label:"User", tooltip:"The username of the user on whose talk page you left a message.", value: prev_page }); work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; case "notice": var noticeboard = work_area.append({ type: "select", name: "noticeboard", label: "Noticeboard:" }); noticeboard.append({ type: "option", label: "WP:AN (Administrators' noticeboard)", value: "an" }); work_area.append({ type:"input", name:"section", label:"Linked thread", tooltip:"The heading of the relevant thread on the noticeboard page.", value: prev_section }); break; case "other": work_area.append({ type:"input", name:"page", label:"Full page name", tooltip:"The full page name where you left the message. For example: 'Wikipedia talk:Twinkle'.", value: prev_page }); work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; case "mail": work_area.append({ type:"input", name:"section", label:"Subject of e-mail (optional)", tooltip:"The subject line of the e-mail you sent." }); break; case "wb": work_area.append({ type:"input", name:"page", label:"User", tooltip:"The username of the user on whose talk page you left a message.", value: prev_page }); work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; } if (value !== "notice") { work_area.append({ type:"textarea", label:"Additional message (optional):", name:"message", tooltip:"An additional message that you would like to leave below the talkback template. Your signature will be added to the end of the message if you leave one." }); } work_area = work_area.render(); root.replaceChild( work_area, old_area ); if (root.message) { root.message.value = prev_message; } }; var callback_evaluate = function( e ) { var tbtarget = e.target.getChecked( "tbtarget" )[0]; var page = null; var section = e.target.section.value; var fullUserTalkPageName = mw.config.get("wgFormattedNamespaces")[ mw.config.get("wgNamespaceIds").user_talk ] + ":" + Morebits.getPageAssociatedUser(); if( tbtarget === "usertalk" || tbtarget === "other" || tbtarget === "wb" ) { page = e.target.page.value; if( tbtarget === "usertalk" ) { if( !page ) { alert("You must specify the username of the user whose talk page you left a message on."); return; } } else { if( !page ) { alert("You must specify the full page name when your message is not on a user talk page."); return; } } } else if (tbtarget === "notice") { page = e.target.noticeboard.value; } var message; if (e.target.message) { message = e.target.message.value; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Morebits.wiki.actionCompleted.redirect = fullUserTalkPageName; Morebits.wiki.actionCompleted.notice = "Talkback complete; reloading talk page in a few seconds"; var talkpage = new Morebits.wiki.page(fullUserTalkPageName, "Adding talkback"); var tbPageName = (tbtarget === "mytalk") ? mw.config.get("wgUserName") : page; var text; if ( tbtarget === "notice" ) { text = "\n\n== " + Twinkle.getFriendlyPref("adminNoticeHeading") + " ==\n"; text += "{{subst:AN-notice|thread=" + section + "|noticeboard=Wikipedia:Administrators' noticeboard}} ~~~~"; talkpage.setEditSummary( "Notice of discussion at [[Wikipedia:Administrators' noticeboard]]" + Twinkle.getPref("summaryAd") ); } else if ( tbtarget === "mail" ) { text = "\n\n==" + Twinkle.getFriendlyPref("mailHeading") + "==\n{{you've got mail|subject="; text += section + "|ts=~~~~~}}"; if( message ) { text += "\n" + message + " ~~~~"; } else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) { text += "\n~~~~"; } talkpage.setEditSummary("Notification: You've got mail" + Twinkle.getPref("summaryAd")); } else if ( tbtarget === "wb" ) { text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{wb|"; text += tbPageName; if( section ) { text += "|" + section; } text += "|ts=~~~~~}}"; if( message ) { text += "\n" + message + " ~~~~"; } else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) { text += "\n~~~~"; } talkpage.setEditSummary("Whisperback" + Twinkle.getPref("summaryAd")); } else { //clean talkback heading: strip section header markers, were erroneously suggested in the documentation text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{tb|"; text += tbPageName; if( section ) { text += "|" + section; } text += "|ts=~~~~~}}"; if( message ) { text += "\n" + message + " ~~~~"; } else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) { text += "\n~~~~"; } talkpage.setEditSummary("Talkback ([[" + (tbtarget === "other" ? "" : "User talk:") + tbPageName + (section ? ("#" + section) : "") + "]])" + Twinkle.getPref("summaryAd")); } talkpage.setAppendText( text ); talkpage.setCreateOption("recreate"); talkpage.setMinorEdit(Twinkle.getFriendlyPref("markTalkbackAsMinor")); talkpage.setFollowRedirect( true ); talkpage.append(); }; }()); /* **************************************** *** friendlywelcome.js: Welcome module **************************************** * Mode of invocation: Tab ("Wel"), or from links on diff pages * Active on: Existing user talk pages, diff pages * Config directives in: FriendlyConfig */ Twinkle.welcome = function friendlywelcome() { if( Morebits.queryString.exists( 'friendlywelcome' ) ) { if( Morebits.queryString.get( 'friendlywelcome' ) === 'auto' ) { Twinkle.welcome.auto(); } else { Twinkle.welcome.semiauto(); } } else { Twinkle.welcome.normal(); } }; Twinkle.welcome.auto = function() { if( Morebits.queryString.get( 'action' ) !== 'edit' ) { // userpage not empty, aborting auto-welcome return; } Twinkle.welcome.welcomeUser(); }; Twinkle.welcome.semiauto = function() { Twinkle.welcome.callback( mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\"") ); }; Twinkle.welcome.normal = function() { if( Morebits.queryString.exists( 'diff' ) ) { // check whether the contributors' talk pages exist yet var $oList = $("#mw-diff-otitle2").find("span.mw-usertoollinks a.new:contains(talk)").first(); var $nList = $("#mw-diff-ntitle2").find("span.mw-usertoollinks a.new:contains(talk)").first(); if( $oList.length > 0 || $nList.length > 0 ) { var spanTag = function( color, content ) { var span = document.createElement( 'span' ); span.style.color = color; span.appendChild( document.createTextNode( content ) ); return span; }; var welcomeNode = document.createElement('strong'); var welcomeLink = document.createElement('a'); welcomeLink.appendChild( spanTag( 'Black', '[' ) ); welcomeLink.appendChild( spanTag( 'Goldenrod', 'welcome' ) ); welcomeLink.appendChild( spanTag( 'Black', ']' ) ); welcomeNode.appendChild(welcomeLink); if( $oList.length > 0 ) { var oHref = $oList.attr("href"); var oWelcomeNode = welcomeNode.cloneNode( true ); oWelcomeNode.firstChild.setAttribute( 'href', oHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) ); $oList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) ); $oList[0].parentNode.parentNode.appendChild( oWelcomeNode ); } if( $nList.length > 0 ) { var nHref = $nList.attr("href"); var nWelcomeNode = welcomeNode.cloneNode( true ); nWelcomeNode.firstChild.setAttribute( 'href', nHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) ); $nList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) ); $nList[0].parentNode.parentNode.appendChild( nWelcomeNode ); } } } if( mw.config.get( 'wgNamespaceNumber' ) === 3 ) { var username = mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes twAddPortletLink( function(){ Twinkle.welcome.callback(username); }, "Wel", "friendly-welcome", "Welcome user" ); } }; Twinkle.welcome.welcomeUser = function welcomeUser() { Morebits.status.init( document.getElementById('bodyContent') ); var params = { value: Twinkle.getFriendlyPref('quickWelcomeTemplate'), article: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '', mode: 'auto' }; Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.welcome.callbacks.main); }; Twinkle.welcome.callback = function friendlywelcomeCallback( uid ) { if( uid === mw.config.get('wgUserName') && !confirm( 'Are you really sure you want to welcome yourself?...' ) ){ return; } var Window = new Morebits.simpleWindow( 600, 420 ); Window.setTitle( "Welcome user" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#welcome" ); var form = new Morebits.quickForm( Twinkle.welcome.callback.evaluate ); form.append({ type: 'select', name: 'type', label: 'Type of welcome: ', event: Twinkle.welcome.populateWelcomeList, list: [ { type: 'option', value: 'standard', label: 'Standard welcomes', selected: !Morebits.isIPAddress(mw.config.get('wgTitle')) }, { type: 'option', value: 'anonymous', label: 'Problem user welcomes', selected: Morebits.isIPAddress(mw.config.get('wgTitle')) } ] }); form.append( { type: 'div', id: 'welcomeWorkArea' } ); form.append( { type: 'input', name: 'article', label: '* Linked article (if supported by template):', value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ), tooltip: 'An article might be linked from within the welcome if the template supports it. Leave empty for no article to be linked. Templates that support a linked article are marked with an asterisk.' } ); var previewlink = document.createElement( 'a' ); $(previewlink).click(function(){ Twinkle.welcome.callbacks.preview(result); // |result| is defined below }); previewlink.style.cursor = "pointer"; previewlink.textContent = 'Preview'; form.append( { type: 'div', name: 'welcomepreview', label: [ previewlink ] } ); form.append( { type: 'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); // initialize the welcome list var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.type.dispatchEvent( evt ); }; Twinkle.welcome.populateWelcomeList = function(e) { var type = e.target.value; var $workarea = $(e.target.form).find("div#welcomeWorkArea"); var div = new Morebits.quickForm.element({ type: "div", id: "welcomeWorkArea" }); if ((type === "standard" || type === "anonymous") && Twinkle.getFriendlyPref("customWelcomeList").length) { div.append({ type: 'header', label: 'Custom welcome templates' }); div.append({ type: 'radio', name: 'template', list: Twinkle.getFriendlyPref("customWelcomeList"), event: Twinkle.welcome.selectTemplate }); } var appendTemplates = function(list) { div.append({ type: 'radio', name: 'template', list: list.map(function(obj) { var properties = Twinkle.welcome.templates[obj]; var result = (properties ? { value: obj, label: "{{" + obj + "}}: " + properties.description + (properties.linkedArticle ? "\u00A0*" : ""), // U+00A0 NO-BREAK SPACE tooltip: properties.tooltip // may be undefined } : { value: obj, label: "{{" + obj + "}}" }); return result; }), event: Twinkle.welcome.selectTemplate }); }; switch (type) { case "standard": div.append({ type: 'header', label: 'General welcome templates' }); appendTemplates([ "welcome", "welcome2", "welcome-anon", "welcome-anon2", "welcome-en", "welcome-iw", "welcomeg", "welcomeq", "welcome-personal", "welcome-school" ]); break; case "anonymous": div.append({ type: 'header', label: 'Problem user welcome templates' }); appendTemplates([ "firstarticle", "welcomespam", "welcomenpov", "welcomevandal" ]); break; default: div.append({ type: 'div', label: 'Twinkle.welcome.populateWelcomeList: something went wrong' }); break; } var rendered = div.render(); rendered.className = "quickform-scrollbox"; $workarea.replaceWith(rendered); var firstRadio = e.target.form.template[0]; firstRadio.checked = true; Twinkle.welcome.selectTemplate({ target: firstRadio }); }; Twinkle.welcome.selectTemplate = function(e) { var properties = Twinkle.welcome.templates[e.target.values]; e.target.form.article.disabled = (properties ? !properties.linkedArticle : false); }; // A list of welcome templates and their properties and syntax // The four fields that are available are "description", "linkedArticle", "syntax", and "tooltip". // The three magic words that can be used in the "syntax" field are: // - $USERNAME$ - replaced by the welcomer's username, depending on user's preferences // - $ARTICLE$ - replaced by an article name, if "linkedArticle" is true // - $HEADER$ - adds a level 2 header (most templates already include this) Twinkle.welcome.templates = { "welcome": { description: "standard plain text welcome", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome2": { description: "welcome with graphic and orange color sheme", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome2|~~~~|art=$ARTICLE$}}" }, "welcome-anon": { description: "welcome anonymous user and suggest getting a username", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome-anon|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome-anon2": { description: "like welcome-anon, but with table and colors", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome-anon2|$USERNAME$|art=$ARTICLE$}}" }, "welcome-en": { description: "welcome for users from main English Wikipedia", linkedArticle: false, syntax: "$HEADER$ {{subst:welcome-en}} ~~~~" }, "welcome-iw": { description: "welcome users from another Wikipedia", linkedArticle: false, syntax: "$HEADER$ {{subst:welcome-iw}} ~~~~" }, "welcomeg": { description: "welcome with blue background", linkedArticle: true, syntax: "{{subst:welcomeg|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcomeq": { description: "like welcomeg but a bit shorter", linkedArticle: true, syntax: "{{subst:welcomeq|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome-personal": { description: "a more personal welcome with a plate of cookies", linkedArticle: true, syntax: "{{subst:welcome-personal|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome-school": { description: "for welcoming students participating in a class project", linkedArticle: false, syntax: "{{subst:welcome-school}} ~~~~" }, // second group "firstarticle": { description: "welcome with note that created page may get deleted", linkedArticle: true, syntax: "$HEADER$ {{subst:firstarticle|1=$ARTICLE$}} ~~~~" }, "welcomespam": { description: "welcome users which did spam changes", linkedArticle: true, syntax: "$HEADER$ {{subst:welcomespam|art=$ARTICLE$}} ~~~~" }, "welcomenpov": { description: "welcome with warning to make changes that fit the NPOV requirements", linkedArticle: true, syntax: "$HEADER$ {{subst:welcomenpov|$ARTICLE$}} ~~~~" }, "welcomevandal": { description: "welcome user which performed vandalism", linkedArticle: true, syntax: "$HEADER$ {{subst:welcomevandal|$ARTICLE$}} ~~~~" } }; Twinkle.welcome.getTemplateWikitext = function(template, article) { var properties = Twinkle.welcome.templates[template]; if (properties) { return properties.syntax. replace("$USERNAME$", Twinkle.getFriendlyPref("insertUsername") ? mw.config.get("wgUserName") : ""). replace("$ARTICLE$", article ? article : ""). replace(/\$HEADER\$\s*/, "== Welcome ==\n\n"). replace("$EXTRA$", ""); // EXTRA is not implemented yet } else { return "{{subst:" + template + (article ? ("|art=" + article) : "") + "}} ~~~~"; } }; Twinkle.welcome.callbacks = { preview: function(form) { var previewDialog = new Morebits.simpleWindow(750, 400); previewDialog.setTitle("Welcome template preview"); previewDialog.setScriptName("Welcome user"); previewDialog.setModality(true); var previewdiv = document.createElement("div"); previewdiv.style.marginLeft = previewdiv.style.marginRight = "0.5em"; previewdiv.style.fontSize = "small"; previewDialog.setContent(previewdiv); var previewer = new Morebits.wiki.preview(previewdiv); previewer.beginRender(Twinkle.welcome.getTemplateWikitext(form.getChecked("template"), form.article.value)); var submit = document.createElement("input"); submit.setAttribute("type", "submit"); submit.setAttribute("value", "Close"); previewDialog.addContent(submit); previewDialog.display(); $(submit).click(function(e) { previewDialog.close(); }); }, main: function( pageobj ) { var params = pageobj.getCallbackParameters(); var text = pageobj.getPageText(); // abort if mode is auto and form is not empty if( pageobj.exists() && params.mode === 'auto' ) { Morebits.status.info( 'Warning', 'User talk page not empty; aborting automatic welcome' ); Morebits.wiki.actionCompleted.event(); return; } var welcomeText = Twinkle.welcome.getTemplateWikitext(params.value, params.article); if( Twinkle.getFriendlyPref('topWelcomes') ) { text = welcomeText + '\n\n' + text; } else { text += "\n" + welcomeText; } var summaryText = "Welcome to Wikipedia!"; pageobj.setPageText(text); pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd')); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchWelcomes')); pageobj.setCreateOption('recreate'); pageobj.save(); } }; Twinkle.welcome.callback.evaluate = function friendlywelcomeCallbackEvaluate(e) { var form = e.target; var params = { value: form.getChecked("template"), article: form.article.value, mode: 'manual' }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.welcome.callbacks.main); }; /* **************************************** *** twinklearv.js: ARV module **************************************** * Mode of invocation: Tab ("ARV") * Active on: Existing and non-existing user pages, user talk pages, contributions pages * Config directives in: TwinkleConfig */ Twinkle.arv = function twinklearv() { var username = Morebits.getPageAssociatedUser(); if ( username === false ) { return; } var title = Morebits.isIPAddress( username ) ? 'Report IP to administrators' : 'Report user to administrators'; twAddPortletLink( function(){ Twinkle.arv.callback(username); }, "VIP", "tw-arv", title ); }; Twinkle.arv.callback = function ( uid ) { if ( !twinkleUserAuthorized ) { alert("Your account is too new to use Twinkle."); return; } if ( uid === mw.config.get('wgUserName') ) { alert( 'You don\'t want to report yourself, do you?' ); return; } var Window = new Morebits.simpleWindow( 600, 500 ); Window.setTitle( "Vandalism in progress" ); //changed title Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#arv" ); var form = new Morebits.quickForm( Twinkle.arv.callback.evaluate ); var categories = form.append( { type: 'select', name: 'category', label: 'Select report type: ', event: Twinkle.arv.callback.changeCategory } ); categories.append( { type: 'option', label: 'Vandalism (WP:VIP)', value: 'aiv' } ); form.append( { type: 'field', label:'Work area', name: 'work_area' } ); form.append( { type:'submit' } ); form.append( { type: 'hidden', name: 'uid', value: uid } ); var result = form.render(); Window.setContent( result ); Window.display(); // We must init the var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.category.dispatchEvent( evt ); }; Twinkle.arv.callback.changeCategory = function (e) { var value = e.target.value; var root = e.target.form; var old_area = Morebits.quickForm.getElements(root, "work_area")[0]; var work_area = null; switch( value ) { case 'aiv': /* falls through */ default: work_area = new Morebits.quickForm.element( { type: 'field', label: 'Report user for vandalism', name: 'work_area' } ); work_area.append( { type: 'input', name: 'page', label: 'Primary linked page: ', tooltip: 'Leave blank to not link to the page in the report', value: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '', event: function(e) { var value = e.target.value; var root = e.target.form; if( value === '' ) { root.badid.disabled = root.goodid.disabled = true; } else { root.badid.disabled = false; root.goodid.disabled = root.badid.value === ''; } } } ); work_area.append( { type: 'input', name: 'badid', label: 'Revision ID for target page when vandalised: ', tooltip: 'Leave blank for no diff link', value: Morebits.queryString.exists( 'vanarticlerevid' ) ? Morebits.queryString.get( 'vanarticlerevid' ) : '', disabled: !Morebits.queryString.exists( 'vanarticle' ), event: function(e) { var value = e.target.value; var root = e.target.form; root.goodid.disabled = value === ''; } } ); work_area.append( { type: 'input', name: 'goodid', label: 'Last good revision ID before vandalism of target page: ', tooltip: 'Leave blank for diff link to previous revision', value: Morebits.queryString.exists( 'vanarticlegoodrevid' ) ? Morebits.queryString.get( 'vanarticlegoodrevid' ) : '', disabled: !Morebits.queryString.exists( 'vanarticle' ) || Morebits.queryString.exists( 'vanarticlerevid' ) } ); work_area.append( { type: 'checkbox', name: 'arvtype', list: [ { label: 'Vandalism after final (level 4 or 4im) warning given', value: 'final' }, { label: 'Vandalism after recent (within 1 day) release of block', value: 'postblock' }, { label: 'Evidently a vandalism-only account', value: 'vandalonly', disabled: Morebits.isIPAddress( root.uid.value ) }, { label: 'Account is evidently a spambot or a compromised account', value: 'spambot' }, { label: 'Account is a promotion-only account', value: 'promoonly' } ] } ); work_area.append( { type: 'textarea', name: 'reason', label: 'Comment: ' } ); work_area = work_area.render(); old_area.parentNode.replaceChild( work_area, old_area ); break; } }; Twinkle.arv.callback.evaluate = function(e) { var form = e.target; var reason = ""; var comment = ""; if ( form.reason ) { comment = form.reason.value; } var uid = form.uid.value; var types; switch( form.category.value ) { // Report user for vandalism case 'aiv': /* falls through */ default: types = form.getChecked( 'arvtype' ); if( !types.length && comment === '' ) { alert( 'You must specify some reason' ); return; } types = types.map( function(v) { switch(v) { case 'final': return 'vandalism after final warning'; case 'postblock': return 'vandalism after recent release of block'; case 'spambot': return 'account is evidently a spambot or a compromised account'; case 'vandalonly': return 'actions evidently indicate a vandalism-only account'; case 'promoonly': return 'account is being used only for promotional purposes'; default: return 'unknown reason'; } } ).join( '; ' ); if ( form.page.value !== '' ) { // add a leading : on linked page namespace to prevent transclusion reason = 'On [[' + form.page.value.replace( /^(Image|Category|File):/i, ':$1:' ) + ']]'; if ( form.badid.value !== '' ) { var query = { 'title': form.page.value, 'diff': form.badid.value, 'oldid': form.goodid.value }; reason += ' ({{diff|' + form.page.value + '|' + form.badid.value + '|' + form.goodid.value + '|diff}})'; } reason += ':'; } if ( types ) { reason += " " + types; } if (comment !== "" ) { reason += (reason === "" ? "" : ". ") + comment; } reason += ". ~~~~"; reason = reason.replace(/\r?\n/g, "\n*:"); // indent newlines Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = "Wikipedia:Vandalism in progress"; Morebits.wiki.actionCompleted.notice = "Reporting complete"; var aivPage = new Morebits.wiki.page( 'Wikipedia:Vandalism in progress', 'Processing VIP request' ); aivPage.setPageSection( 3 ); aivPage.setFollowRedirect( true ); aivPage.load( function() { var text = aivPage.getPageText(); // check if user has already been reported if (new RegExp( "\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*" + RegExp.escape( uid, true ) + "\\s*\\}\\}" ).test(text)) { aivPage.getStatusElement().info( 'Report already present, will not add a new one' ); return; } aivPage.getStatusElement().status( 'Adding new report...' ); aivPage.setEditSummary( 'Reporting [[Special:Contributions/' + uid + '|' + uid + ']].' + Twinkle.getPref('summaryAd') ); aivPage.setAppendText( '\n*{{' + ( Morebits.isIPAddress( uid ) ? 'IPvandal' : 'vandal' ) + '|' + (/\=/.test( uid ) ? '1=' : '' ) + uid + '}} &ndash; ' + reason ); aivPage.append(); } ); break; } }; /* **************************************** *** twinklebatchdelete.js: Batch delete module (sysops only) **************************************** * Mode of invocation: Tab ("D-batch") * Active on: Existing and non-existing non-articles, and Special:PrefixIndex * Config directives in: TwinkleConfig */ Twinkle.batchdelete = function twinklebatchdelete() { if( Morebits.userIsInGroup( 'sysop' ) && (mw.config.get( 'wgNamespaceNumber' ) > 0 || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) { twAddPortletLink( Twinkle.batchdelete.callback, "D-batch", "tw-batch", "Delete pages found in this category/on this page" ); } }; Twinkle.batchdelete.unlinkCache = {}; Twinkle.batchdelete.callback = function twinklebatchdeleteCallback() { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setTitle( "Batch deletion" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#batchdelete" ); var form = new Morebits.quickForm( Twinkle.batchdelete.callback.evaluate ); form.append( { type: 'checkbox', list: [ { label: 'Delete pages', name: 'delete_page', value: 'delete', checked: true }, { label: 'Remove backlinks to the page', name: 'unlink_page', value: 'unlink', checked: false }, { label: 'Delete redirects to deleted pages', name: 'delete_redirects', value: 'delete_redirects', checked: true } ] } ); form.append( { type: 'textarea', name: 'reason', label: 'Reason: ' } ); var query; if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // Category: query = { 'action': 'query', 'generator': 'categorymembers', 'gcmtitle': mw.config.get( 'wgPageName' ), 'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': [ 'categories', 'revisions' ], 'rvprop': [ 'size' ] }; } else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) { var gapnamespace, gapprefix; if(Morebits.queryString.exists( 'from' ) ) { gapnamespace = Morebits.queryString.get( 'namespace' ); gapprefix = Morebits.string.toUpperCaseFirstChar( Morebits.queryString.get( 'from' ) ); } else { var pathSplit = location.pathname.split('/'); if (pathSplit.length < 3 || pathSplit[2] !== "Special:PrefixIndex") { return; } var titleSplit = pathSplit[3].split(':'); gapnamespace = mw.config.get("wgNamespaceIds")[titleSplit[0].toLowerCase()]; if ( titleSplit.length < 2 || typeof gapnamespace === 'undefined' ) { gapnamespace = 0; // article namespace gapprefix = pathSplit.splice(3).join('/'); } else { pathSplit = pathSplit.splice(4); pathSplit.splice(0,0,titleSplit.splice(1).join(':')); gapprefix = pathSplit.join('/'); } } query = { 'action': 'query', 'generator': 'allpages', 'gapnamespace': gapnamespace , 'gapprefix': gapprefix, 'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop' : ['categories', 'revisions' ], 'rvprop': [ 'size' ] }; } else { query = { 'action': 'query', 'generator': 'links', 'titles': mw.config.get( 'wgPageName' ), 'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': [ 'categories', 'revisions' ], 'rvprop': [ 'size' ] }; } var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//page[@ns != "6" and not(@missing)]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); // 6 = File: namespace var list = []; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var object = snapshot.snapshotItem(i); var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue; var size = xmlDoc.evaluate( 'revisions/rev/@size', object, null, XPathResult.NUMBER_TYPE, null ).numberValue; var disputed = xmlDoc.evaluate( 'boolean(categories/cl[@title="Category:Contested candidates for speedy deletion"])', object, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue; list.push( {label:page + ' (' + size + ' bytes)' + ( disputed ? ' (DISPUTED CSD)' : '' ), value:page, checked:!disputed }); } self.params.form.append( { type: 'checkbox', name: 'pages', list: list } ); self.params.form.append( { type:'submit' } ); var result = self.params.form.render(); self.params.Window.setContent( result ); } ); wikipedia_api.params = { form:form, Window:Window }; wikipedia_api.post(); var root = document.createElement( 'div' ); Morebits.status.init( root ); Window.setContent( root ); Window.display(); }; Twinkle.batchdelete.currentDeleteCounter = 0; Twinkle.batchdelete.currentUnlinkCounter = 0; Twinkle.batchdelete.currentdeletor = 0; Twinkle.batchdelete.callback.evaluate = function twinklebatchdeleteCallbackEvaluate(event) { Morebits.wiki.actionCompleted.notice = 'Status'; Morebits.wiki.actionCompleted.postfix = 'batch deletion is now complete'; mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var pages = event.target.getChecked( 'pages' ); var reason = event.target.reason.value; var delete_page = event.target.delete_page.checked; var unlink_page = event.target.unlink_page.checked; var delete_redirects = event.target.delete_redirects.checked; if( ! reason ) { return; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( event.target ); if( !pages ) { Morebits.status.error( 'Error', 'nothing to delete, aborting' ); return; } function toCall( work ) { if( work.length === 0 && Twinkle.batchdelete.currentDeleteCounter <= 0 && Twinkle.batchdelete.currentUnlinkCounter <= 0 ) { window.clearInterval( Twinkle.batchdelete.currentdeletor ); Morebits.wiki.removeCheckpoint(); return; } else if( work.length !== 0 && ( Twinkle.batchdelete.currentDeleteCounter <= Twinkle.getPref('batchDeleteMinCutOff') || Twinkle.batchdelete.currentUnlinkCounter <= Twinkle.getPref('batchDeleteMinCutOff') ) ) { Twinkle.batchdelete.unlinkCache = []; // Clear the cache var pages = work.shift(); Twinkle.batchdelete.currentDeleteCounter += pages.length; Twinkle.batchdelete.currentUnlinkCounter += pages.length; for( var i = 0; i < pages.length; ++i ) { var page = pages[i]; var query = { 'action': 'query', 'titles': page }; var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchdelete.callbacks.main ); wikipedia_api.params = { page:page, reason:reason, unlink_page:unlink_page, delete_page:delete_page, delete_redirects:delete_redirects }; wikipedia_api.post(); } } } var work = Morebits.array.chunk( pages, Twinkle.getPref('batchdeleteChunks') ); Morebits.wiki.addCheckpoint(); Twinkle.batchdelete.currentdeletor = window.setInterval( toCall, 1000, work ); }; Twinkle.batchdelete.callbacks = { main: function( self ) { var xmlDoc = self.responseXML; var normal = xmlDoc.evaluate( '//normalized/n/@to', xmlDoc, null, XPathResult.STRING_TYPE, null ).stringValue; if( normal ) { self.params.page = normal; } var exists = xmlDoc.evaluate( 'boolean(//pages/page[not(@missing)])', xmlDoc, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue; if( ! exists ) { self.statelem.error( "It seems that the page doesn't exist, perhaps it has already been deleted" ); return; } var query, wikipedia_api; if( self.params.unlink_page ) { query = { 'action': 'query', 'list': 'backlinks', 'blfilterredir': 'nonredirects', 'blnamespace': [0, 100], // main space and portal space only 'bltitle': self.params.page, 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops }; wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.batchdelete.callbacks.unlinkBacklinksMain ); wikipedia_api.params = self.params; wikipedia_api.post(); } else { --Twinkle.batchdelete.currentUnlinkCounter; } if( self.params.delete_page ) { if (self.params.delete_redirects) { query = { 'action': 'query', 'list': 'backlinks', 'blfilterredir': 'redirects', 'bltitle': self.params.page, 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops }; wikipedia_api = new Morebits.wiki.api( 'Grabbing redirects', query, Twinkle.batchdelete.callbacks.deleteRedirectsMain ); wikipedia_api.params = self.params; wikipedia_api.post(); } var wikipedia_page = new Morebits.wiki.page( self.params.page, 'Deleting page ' + self.params.page ); wikipedia_page.setEditSummary(self.params.reason + Twinkle.getPref('deletionSummaryAd')); wikipedia_page.deletePage(function( apiobj ) { --Twinkle.batchdelete.currentDeleteCounter; var link = document.createElement( 'a' ); link.setAttribute( 'href', mw.util.getUrl(self.params.page) ); link.setAttribute( 'title', self.params.page ); link.appendChild( document.createTextNode( self.params.page ) ); apiobj.statelem.info( [ 'completed (' , link , ')' ] ); } ); } else { --Twinkle.batchdelete.currentDeleteCounter; } }, deleteRedirectsMain: function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); var total = snapshot.snapshotLength; if( snapshot.snapshotLength === 0 ) { return; } var statusIndicator = new Morebits.status('Deleting redirects for ' + self.params.page, '0%'); var onsuccess = function( self ) { var obj = self.params.obj; var total = self.params.total; var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%'; obj.update( now ); self.statelem.unlink(); if( self.params.current >= total ) { obj.info( now + ' (completed)' ); Morebits.wiki.removeCheckpoint(); } }; Morebits.wiki.addCheckpoint(); if( snapshot.snapshotLength === 0 ) { statusIndicator.info( '100% (completed)' ); Morebits.wiki.removeCheckpoint(); return; } var params = $.extend({}, self.params); params.current = 0; params.total = total; params.obj = statusIndicator; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var title = snapshot.snapshotItem(i).value; var wikipedia_page = new Morebits.wiki.page( title, "Deleting " + title ); wikipedia_page.setEditSummary('[[WP:QD#G8|G8]]: Redirect to deleted page "' + self.params.page + '"' + Twinkle.getPref('deletionSummaryAd')); wikipedia_page.setCallbackParameters(params); wikipedia_page.deletePage(onsuccess); } }, unlinkBacklinksMain: function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); if( snapshot.snapshotLength === 0 ) { --Twinkle.batchdelete.currentUnlinkCounter; return; } var statusIndicator = new Morebits.status('Unlinking backlinks to ' + self.params.page, '0%'); var total = snapshot.snapshotLength * 2; var onsuccess = function( self ) { var obj = self.params.obj; var total = self.params.total; var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%'; obj.update( now ); self.statelem.unlink(); if( self.params.current >= total ) { obj.info( now + ' (completed)' ); --Twinkle.batchdelete.currentUnlinkCounter; Morebits.wiki.removeCheckpoint(); } }; Morebits.wiki.addCheckpoint(); if( snapshot.snapshotLength === 0 ) { statusIndicator.info( '100% (completed)' ); --Twinkle.batchdelete.currentUnlinkCounter; Morebits.wiki.removeCheckpoint(); return; } self.params.total = total; self.params.obj = statusIndicator; self.params.current = 0; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var title = snapshot.snapshotItem(i).value; var wikipedia_page = new Morebits.wiki.page( title, "Unlinking on " + title ); var params = $.extend( {}, self.params ); params.title = title; params.onsuccess = onsuccess; wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.batchdelete.callbacks.unlinkBacklinks); } }, unlinkBacklinks: function( pageobj ) { var params = pageobj.getCallbackParameters(); if( ! pageobj.exists() ) { // we probably just deleted it, as a recursive backlink params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } ); Morebits.wiki.actionCompleted(); return; } var text; if( params.title in Twinkle.batchdelete.unlinkCache ) { text = Twinkle.batchdelete.unlinkCache[ params.title ]; } else { text = pageobj.getPageText(); } var old_text = text; var wikiPage = new Morebits.wikitext.page( text ); wikiPage.removeLink( params.page ); text = wikiPage.getText(); Twinkle.batchdelete.unlinkCache[ params.title ] = text; if( text === old_text ) { // Nothing to do, return params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } ); Morebits.wiki.actionCompleted(); return; } pageobj.setEditSummary('Removing link(s) to deleted page ' + self.params.page + Twinkle.getPref('deletionSummaryAd')); pageobj.setPageText(text); pageobj.setCreateOption('nocreate'); pageobj.save(params.onsuccess); } }; /* **************************************** *** twinklebatchprotect.js: Batch protect module (sysops only) **************************************** * Mode of invocation: Tab ("P-batch") * Active on: Existing project pages and user pages; existing and * non-existing categories; Special:PrefixIndex * Config directives in: TwinkleConfig */ Twinkle.batchprotect = function twinklebatchprotect() { if( Morebits.userIsInGroup( 'sysop' ) && ((mw.config.get( 'wgArticleId' ) > 0 && (mw.config.get( 'wgNamespaceNumber' ) === 2 || mw.config.get( 'wgNamespaceNumber' ) === 4)) || mw.config.get( 'wgNamespaceNumber' ) === 14 || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) { twAddPortletLink( Twinkle.batchprotect.callback, "P-batch", "tw-pbatch", "Protect pages linked from this page" ); } }; Twinkle.batchprotect.unlinkCache = {}; Twinkle.batchprotect.callback = function twinklebatchprotectCallback() { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setTitle( "Batch protection" ); Window.setScriptName( "Twinkle" ); //Window.addFooterLink( "Protection templates", "Template:Protection templates" ); Window.addFooterLink( "Protection policy", "WP:PROT" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#protect" ); var form = new Morebits.quickForm( Twinkle.batchprotect.callback.evaluate ); form.append({ type: 'checkbox', name: 'editmodify', event: Twinkle.protect.formevents.editmodify, list: [ { label: 'Modify edit protection', value: 'editmodify', tooltip: 'Only for existing pages.', checked: true } ] }); var editlevel = form.append({ type: 'select', name: 'editlevel', label: 'Edit protection:', event: Twinkle.protect.formevents.editlevel }); editlevel.append({ type: 'option', label: 'All', value: 'all' }); editlevel.append({ type: 'option', label: 'Autoconfirmed', value: 'autoconfirmed' }); editlevel.append({ type: 'option', label: 'Sysop', value: 'sysop', selected: true }); form.append({ type: 'select', name: 'editexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, list: [ { label: '1 hour', value: '1 hour' }, { label: '2 hours', value: '2 hours' }, { label: '3 hours', value: '3 hours' }, { label: '6 hours', value: '6 hours' }, { label: '12 hours', value: '12 hours' }, { label: '1 day', value: '1 day' }, { label: '2 days', selected: true, value: '2 days' }, { label: '3 days', value: '3 days' }, { label: '4 days', value: '4 days' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '2 months', value: '2 months' }, { label: '3 months', value: '3 months' }, { label: '1 year', value: '1 year' }, { label: 'indefinite', value:'indefinite' }, { label: 'Custom...', value: 'custom' } ] }); form.append({ type: 'checkbox', name: 'movemodify', event: Twinkle.protect.formevents.movemodify, list: [ { label: 'Modify move protection', value: 'movemodify', tooltip: 'Only for existing pages.', checked: true } ] }); var movelevel = form.append({ type: 'select', name: 'movelevel', label: 'Move protection:', event: Twinkle.protect.formevents.movelevel }); movelevel.append({ type: 'option', label: 'All', value: 'all' }); movelevel.append({ type: 'option', label: 'Autoconfirmed', value: 'autoconfirmed' }); movelevel.append({ type: 'option', label: 'Sysop', value: 'sysop', selected: true }); form.append({ type: 'select', name: 'moveexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, list: [ { label: '1 hour', value: '1 hour' }, { label: '2 hours', value: '2 hours' }, { label: '3 hours', value: '3 hours' }, { label: '6 hours', value: '6 hours' }, { label: '12 hours', value: '12 hours' }, { label: '1 day', value: '1 day' }, { label: '2 days', selected: true, value: '2 days' }, { label: '3 days', value: '3 days' }, { label: '4 days', value: '4 days' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '2 months', value: '2 months' }, { label: '3 months', value: '3 months' }, { label: '1 year', value: '1 year' }, { label: 'indefinite', value:'indefinite' }, { label: 'Custom...', value: 'custom' } ] }); form.append({ type: 'checkbox', name: 'createmodify', event: function twinklebatchprotectFormCreatemodifyEvent(e) { e.target.form.createlevel.disabled = !e.target.checked; e.target.form.createexpiry.disabled = !e.target.checked || (e.target.form.createlevel.value === 'all'); e.target.form.createlevel.style.color = e.target.form.createexpiry.style.color = (e.target.checked ? "" : "transparent"); }, list: [ { label: 'Modify create protection', value: 'createmodify', tooltip: 'Only for pages that do not exist.', checked: true } ] }); var createlevel = form.append({ type: 'select', name: 'createlevel', label: 'Create protection:', event: Twinkle.protect.formevents.createlevel }); createlevel.append({ type: 'option', label: 'All', value: 'all' }); createlevel.append({ type: 'option', label: 'Autoconfirmed', value: 'autoconfirmed' }); createlevel.append({ type: 'option', label: 'Sysop', value: 'sysop', selected: true }); form.append({ type: 'select', name: 'createexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, list: [ { label: '1 hour', value: '1 hour' }, { label: '2 hours', value: '2 hours' }, { label: '3 hours', value: '3 hours' }, { label: '6 hours', value: '6 hours' }, { label: '12 hours', value: '12 hours' }, { label: '1 day', value: '1 day' }, { label: '2 days', value: '2 days' }, { label: '3 days', value: '3 days' }, { label: '4 days', value: '4 days' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '2 months', value: '2 months' }, { label: '3 months', value: '3 months' }, { label: '1 year', value: '1 year' }, { label: 'indefinite', selected: true, value: 'indefinite' }, { label: 'Custom...', value: 'custom' } ] }); form.append( { type: 'textarea', name: 'reason', label: 'Reason (for protection log): ' } ); var query; if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // categories query = { 'action': 'query', 'generator': 'categorymembers', 'gcmtitle': mw.config.get( 'wgPageName' ), 'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': 'revisions', 'rvprop': 'size' }; } else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) { query = { 'action': 'query', 'generator': 'allpages', 'gapnamespace': Morebits.queryString.exists('namespace') ? Morebits.queryString.get( 'namespace' ) : document.getElementById('namespace').value, 'gapprefix': Morebits.queryString.exists('from') ? Morebits.string.toUpperCaseFirstChar(Morebits.queryString.get( 'from' ).replace('+', ' ')) : Morebits.string.toUpperCaseFirstChar(document.getElementById('nsfrom').value), 'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': 'revisions', 'rvprop': 'size' }; } else { query = { 'action': 'query', 'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops 'generator': 'links', 'titles': mw.config.get( 'wgPageName' ), 'prop': 'revisions', 'rvprop': 'size' }; } var statusdiv = document.createElement("div"); statusdiv.style.padding = '15px'; // just so it doesn't look broken Window.setContent(statusdiv); Morebits.status.init(statusdiv); Window.display(); var statelem = new Morebits.status("Grabbing list of pages"); var wikipedia_api = new Morebits.wiki.api( 'loading...', query, function(apiobj) { var xml = apiobj.responseXML; var $pages = $(xml).find('page'); var list = []; $pages.each(function(index, page) { var $page = $(page); var title = $page.attr('title'); var isRedir = $page.attr('redirect') === ""; // XXX ?? var missing = $page.attr('missing') === ""; // XXX ?? var size = $page.find('rev').attr('size'); var metadata = []; if (missing) { metadata.push("page does not exist"); } else { if (isRedir) { metadata.push("redirect"); } metadata.push(size + " bytes"); } list.push( { label: title + (metadata.length ? (' (' + metadata.join('; ') + ')') : '' ), value: title, checked: true }); }); form.append({ type: 'header', label: 'Pages to protect' }); form.append( { type: 'checkbox', name: 'pages', list: list } ); form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); }, statelem ); wikipedia_api.post(); }; Twinkle.batchprotect.currentProtectCounter = 0; Twinkle.batchprotect.currentprotector = 0; Twinkle.batchprotect.callback.evaluate = function twinklebatchprotectCallbackEvaluate(event) { var pages = event.target.getChecked( 'pages' ); var reason = event.target.reason.value; var editmodify = event.target.editmodify.checked; var editlevel = event.target.editlevel.value; var editexpiry = event.target.editexpiry.value; var movemodify = event.target.movemodify.checked; var movelevel = event.target.movelevel.value; var moveexpiry = event.target.moveexpiry.value; var createmodify = event.target.createmodify.checked; var createlevel = event.target.createlevel.value; var createexpiry = event.target.createexpiry.value; if( ! reason ) { alert("You've got to give a reason, you rouge admin!"); return; } Morebits.simpleWindow.setButtonsEnabled(false); Morebits.status.init( event.target ); if( !pages ) { Morebits.status.error( 'Error', 'Nothing to protect, aborting' ); return; } var toCall = function twinklebatchprotectToCall( work ) { if( work.length === 0 && Twinkle.batchprotect.currentProtectCounter <= 0 ) { Morebits.status.info( 'work done' ); window.clearInterval( Twinkle.batchprotect.currentprotector ); Twinkle.batchprotect.currentprotector = Twinkle.batchprotect.currentProtectCounter = 0; Morebits.wiki.removeCheckpoint(); return; } else if( work.length !== 0 && Twinkle.batchprotect.currentProtectCounter <= Twinkle.getPref('batchProtectMinCutOff') ) { var pages = work.shift(); Twinkle.batchprotect.currentProtectCounter += pages.length; for( var i = 0; i < pages.length; ++i ) { var page = pages[i]; var query = { 'action': 'query', 'titles': page }; var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchprotect.callbacks.main ); wikipedia_api.params = { page: page, reason: reason, editmodify: editmodify, editlevel: editlevel, editexpiry: editexpiry, movemodify: movemodify, movelevel: movelevel, moveexpiry: moveexpiry, createmodify: createmodify, createlevel: createlevel, createexpiry: createexpiry }; wikipedia_api.post(); } } }; var work = Morebits.array.chunk( pages, Twinkle.getPref('batchProtectChunks') ); Morebits.wiki.addCheckpoint(); Twinkle.batchprotect.currentprotector = window.setInterval( toCall, 1000, work ); }; Twinkle.batchprotect.callbacks = { main: function( apiobj ) { var xml = apiobj.responseXML; var normal = $(xml).find('normalized n').attr('to'); if( normal ) { apiobj.params.page = normal; } var exists = ($(xml).find('page').attr('missing') !== ""); var page = new Morebits.wiki.page(apiobj.params.page, "Protecting " + apiobj.params.page); var takenAction = false; if (exists && apiobj.params.editmodify) { page.setEditProtection(apiobj.params.editlevel, apiobj.params.editexpiry); takenAction = true; } if (exists && apiobj.params.movemodify) { page.setMoveProtection(apiobj.params.movelevel, apiobj.params.moveexpiry); takenAction = true; } if (!exists && apiobj.params.createmodify) { page.setCreateProtection(apiobj.params.createlevel, apiobj.params.createexpiry); takenAction = true; } if (!takenAction) { Morebits.status.warn("Protecting " + apiobj.params.page, "page " + (exists ? "exists" : "does not exist") + "; nothing to do, skipping"); return; } page.setEditSummary(apiobj.params.reason); page.protect(function(pageobj) { --Twinkle.batchprotect.currentProtectCounter; var link = document.createElement( 'a' ); link.setAttribute( 'href', mw.util.getUrl( apiobj.params.page ) ); link.appendChild( document.createTextNode( apiobj.params.page ) ); pageobj.getStatusElement().info( [ 'completed (' , link , ')' ] ); } ); } }; /* **************************************** *** twinklebatchundelete.js: Batch undelete module **************************************** * Mode of invocation: Tab ("Und-batch") * Active on: Existing and non-existing user pages (??? why?) * Config directives in: TwinkleConfig */ // XXX TODO this module needs to be overhauled to use Morebits.wiki.page Twinkle.batchundelete = function twinklebatchundelete() { if( mw.config.get("wgNamespaceNumber") !== mw.config.get("wgNamespaceIds").user ) { return; } if( Morebits.userIsInGroup( 'sysop' ) ) { twAddPortletLink( Twinkle.batchundelete.callback, "Und-batch", "tw-batch-undel", "Undelete 'em all" ); } }; Twinkle.batchundelete.callback = function twinklebatchundeleteCallback() { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setScriptName("Twinkle"); Window.setTitle("Batch undelete") var form = new Morebits.quickForm( Twinkle.batchundelete.callback.evaluate ); form.append( { type: 'textarea', name: 'reason', label: 'Reason: ' } ); var query = { 'action': 'query', 'generator': 'links', 'titles': mw.config.get("wgPageName"), 'gpllimit' : Twinkle.getPref('batchMax') // the max for sysops }; var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//page[@missing]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); var list = []; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var object = snapshot.snapshotItem(i); var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue; list.push( {label:page, value:page, checked: true }); } self.params.form.append( { type: 'checkbox', name: 'pages', list: list } ); self.params.form.append( { type:'submit' } ); var result = self.params.form.render(); self.params.Window.setContent( result ); } ); wikipedia_api.params = { form:form, Window:Window }; wikipedia_api.post(); var root = document.createElement( 'div' ); Morebits.status.init( root ); Window.setContent( root ); Window.display(); }; Twinkle.batchundelete.currentUndeleteCounter = 0; Twinkle.batchundelete.currentundeletor = 0; Twinkle.batchundelete.callback.evaluate = function( event ) { Morebits.wiki.actionCompleted.notice = 'Status'; Morebits.wiki.actionCompleted.postfix = 'batch undeletion is now completed'; var pages = event.target.getChecked( 'pages' ); var reason = event.target.reason.value; if( ! reason ) { alert("You need to give a reason, you cabal crony!"); return; } Morebits.simpleWindow.setButtonsEnabled(false); Morebits.status.init( event.target ); if( !pages ) { Morebits.status.error( 'Error', 'nothing to undelete, aborting' ); return; } var work = Morebits.array.chunk( pages, Twinkle.getPref('batchUndeleteChunks') ); Morebits.wiki.addCheckpoint(); Twinkle.batchundelete.currentundeletor = window.setInterval( Twinkle.batchundelete.callbacks.main, 1000, work, reason ); }; Twinkle.batchundelete.callbacks = { main: function( work, reason ) { if( work.length === 0 && Twinkle.batchundelete.currentUndeleteCounter <= 0 ) { Morebits.status.info( 'work done' ); window.clearInterval( Twinkle.batchundelete.currentundeletor ); Morebits.wiki.removeCheckpoint(); return; } else if( work.length !== 0 && Twinkle.batchundelete.currentUndeleteCounter <= Twinkle.getPref('batchUndeleteMinCutOff') ) { var pages = work.shift(); Twinkle.batchundelete.currentUndeleteCounter += pages.length; for( var i = 0; i < pages.length; ++i ) { var title = pages[i]; var query = { 'token': mw.user.tokens.get().editToken, 'title': title, 'action': 'undelete', 'reason': reason + Twinkle.getPref('deletionSummaryAd') }; var wikipedia_api = new Morebits.wiki.api( "Undeleting " + title, query, function( self ) { --Twinkle.batchundelete.currentUndeleteCounter; var link = document.createElement( 'a' ); link.setAttribute( 'href', mw.util.getUrl(self.itsTitle) ); link.setAttribute( 'title', self.itsTitle ); link.appendChild( document.createTextNode(self.itsTitle) ); self.statelem.info( ['completed (',link,')'] ); }); wikipedia_api.itsTitle = title; wikipedia_api.post(); } } } }; /* **************************************** *** twinkleconfig.js: Preferences module **************************************** * Mode of invocation: Adds configuration form to Wikipedia:Twinkle/Preferences and user subpages named "/Twinkle preferences", and adds ad box to the top of user subpages belonging to the currently logged-in user which end in '.js' * Active on: What I just said. Yeah. * Config directives in: TwinkleConfig I, [[User:This, that and the other]], originally wrote this. If the code is misbehaving, or you have any questions, don't hesitate to ask me. (This doesn't at all imply [[WP:OWN]]ership - it's just meant to point you in the right direction.) -- TTO */ Twinkle.config = {}; Twinkle.config.commonEnums = { watchlist: { yes: "Add to watchlist", no: "Don't add to watchlist", "default": "Follow your site preferences" }, talkPageMode: { window: "In a window, replacing other user talks", tab: "In a new tab", blank: "In a totally new window" } }; Twinkle.config.commonSets = { csdCriteria: { db: "Custom rationale" }, csdCriteriaDisplayOrder: [ "db" ], csdCriteriaNotificationDisplayOrder: [ "db" ], csdAndDICriteria: { db: "Custom rationale" }, csdAndDICriteriaDisplayOrder: [ "db" ], namespacesNoSpecial: { "0": "Article", "1": "Talk (article)", "2": "User", "3": "User talk", "4": "Wikipedia", "5": "Wikipedia talk", "6": "File", "7": "File talk", "8": "MediaWiki", "9": "MediaWiki talk", "10": "Template", "11": "Template talk", "12": "Help", "13": "Help talk", "14": "Category", "15": "Category talk" } }; /** * Section entry format: * * { * title: <human-readable section title>, * adminOnly: <true for admin-only sections>, * hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>, * inFriendlyConfig: <true for preferences located under FriendlyConfig rather than TwinkleConfig>, * preferences: [ * { * name: <TwinkleConfig property name>, * label: <human-readable short description - used as a form label>, * helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.> * adminOnly: <true for admin-only preferences>, * type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }), * enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>, * setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>, * setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>, * customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>, * customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor> * }, * . . . * ] * }, * . . . * */ Twinkle.config.sections = [ { title: "General", preferences: [ // TwinkleConfig.summaryAd (string) // Text to be appended to the edit summary of edits made using Twinkle { name: "summaryAd", label: "\"Ad\" to be appended to Twinkle's edit summaries", helptip: "The summary ad should start with a space, and be kept short.", type: "string" }, // TwinkleConfig.deletionSummaryAd (string) // Text to be appended to the edit summary of deletions made using Twinkle { name: "deletionSummaryAd", label: "Summary ad to use for deletion summaries", helptip: "Normally the same as the edit summary ad above.", adminOnly: true, type: "string" }, // TwinkleConfig.protectionSummaryAd (string) // Text to be appended to the edit summary of page protections made using Twinkle { name: "protectionSummaryAd", label: "Summary ad to use for page protections", helptip: "Normally the same as the edit summary ad above.", adminOnly: true, type: "string" }, // TwinkleConfig.userTalkPageMode may take arguments: // 'window': open a new window, remember the opened window // 'tab': opens in a new tab, if possible. // 'blank': force open in a new window, even if such a window exists { name: "userTalkPageMode", label: "When opening a user talk page, open it", type: "enum", enumValues: Twinkle.config.commonEnums.talkPageMode }, // TwinkleConfig.dialogLargeFont (boolean) { name: "dialogLargeFont", label: "Use larger text in Twinkle dialogs", type: "boolean" } ] }, { title: "Revert and rollback", // twinklefluff module preferences: [ // TwinkleConfig.openTalkPage (array) // What types of actions that should result in opening of talk page { name: "openTalkPage", label: "Open user talk page after these types of reversions", type: "set", setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" } }, // TwinkleConfig.openTalkPageOnAutoRevert (bool) // Defines if talk page should be opened when calling revert from contrib page, because from there, actions may be multiple, and opening talk page not suitable. If set to true, openTalkPage defines then if talk page will be opened. { name: "openTalkPageOnAutoRevert", label: "Open user talk page when invoking rollback from user contributions", helptip: "Often, you may be rolling back many pages at a time from a vandal's contributions page, so it would be unsuitable to open the user talk page. Hence, this option is off by default. When this is on, the desired options must be enabled in the previous setting for this to work.", type: "boolean" }, // TwinkleConfig.markRevertedPagesAsMinor (array) // What types of actions that should result in marking edit as minor { name: "markRevertedPagesAsMinor", label: "Mark as minor edit for these types of reversions", type: "set", setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" } }, // TwinkleConfig.watchRevertedPages (array) // What types of actions that should result in forced addition to watchlist { name: "watchRevertedPages", label: "Add pages to watchlist for these types of reversions", type: "set", setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" } }, // TwinkleConfig.offerReasonOnNormalRevert (boolean) // If to offer a prompt for extra summary reason for normal reverts, default to true { name: "offerReasonOnNormalRevert", label: "Prompt for reason for normal rollbacks", helptip: "\"Normal\" rollbacks are the ones that are invoked from the middle [rollback] link.", type: "boolean" }, { name: "confirmOnFluff", label: "Provide a confirmation message before reverting", helptip: "For users of pen or touch devices, and chronically indecisive people.", type: "boolean" }, // TwinkleConfig.showRollbackLinks (array) // Where Twinkle should show rollback links (diff, others, mine, contribs) // Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres { name: "showRollbackLinks", label: "Show rollback links on these pages", type: "set", setValues: { diff: "Diff pages", others: "Contributions pages of other users", mine: "My contributions page" } } ] }, { title: "Deletion tagging", preferences: [ { name: "speedySelectionStyle", label: "When to go ahead and tag/delete the page", type: "enum", enumValues: { "buttonClick": 'When I click "Submit"', "radioClick": "As soon as I click an option" } }, // TwinkleConfig.markSpeedyPagesAsPatrolled (boolean) // If, when applying speedy template to page, to mark the page as patrolled (if the page was reached from NewPages) { name: "markSpeedyPagesAsPatrolled", label: "Mark page as patrolled when tagging (if possible)", helptip: "Due to technical limitations, pages are only marked as patrolled when they are reached via Special:NewPages.", type: "boolean" }, // TwinkleConfig.openUserTalkPageOnSpeedyDelete (array of strings) // What types of actions that should result user talk page to be opened when speedily deleting (admin only) { name: "openUserTalkPageOnSpeedyDelete", label: "Open user talk page when deleting under these criteria", adminOnly: true, type: "set", setValues: Twinkle.config.commonSets.csdAndDICriteria, setDisplayOrder: Twinkle.config.commonSets.csdAndDICriteriaDisplayOrder }, // TwinkleConfig.deleteTalkPageOnDelete (boolean) // If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only) { name: "deleteTalkPageOnDelete", label: "Check the \"also delete talk page\" box by default", adminOnly: true, type: "boolean" }, // TwinkleConfig.deleteSysopDefaultToTag (boolean) // Make the CSD screen default to "tag" instead of "delete" (admin only) { name: "deleteSysopDefaultToTag", label: "Default to tagging instead of outright deletion", adminOnly: true, type: "boolean" }, // TwinkleConfig.speedyWindowWidth (integer) // Defines the width of the Twinkle SD window in pixels { name: "speedyWindowWidth", label: "Width of deletion window (pixels)", type: "integer" }, // TwinkleConfig.speedyWindowWidth (integer) // Defines the width of the Twinkle SD window in pixels { name: "speedyWindowHeight", label: "Height of deletion window (pixels)", helptip: "If you have a big monitor, you might like to increase this.", type: "integer" }, { name: "logSpeedyNominations", label: "Keep a log in userspace of all deletion nominations", helptip: "Since non-admins do not have access to their deleted contributions, the userspace log offers a good way to keep track of all pages you nominate for QD using Twinkle. Files tagged using DI are also added to this log.", type: "boolean" }, { name: "speedyLogPageName", label: "Keep the deletion userspace log at this user subpage", helptip: "i.e. User:<i>username</i>/<i>subpage name</i>. Only works if you turn on the deletion userspace log.", type: "string" } ] }, { title: "Unlink", preferences: [ // TwinkleConfig.unlinkNamespaces (array) // In what namespaces unlink should happen, default in 0 (article) and 100 (portal) { name: "unlinkNamespaces", label: "Remove links from pages in these namespaces", helptip: "Avoid selecting any talk namespaces, as Twinkle might end up unlinking on talk archives (a big no-no).", type: "set", setValues: Twinkle.config.commonSets.namespacesNoSpecial } ] }, { title: "Hidden", hidden: true, preferences: [ // twinkle.header.js: portlet setup { name: "portletArea", type: "string" }, { name: "portletId", type: "string" }, { name: "portletName", type: "string" }, { name: "portletType", type: "string" }, { name: "portletNext", type: "string" }, // twinklefluff.js: defines how many revision to query maximum, maximum possible is 50, default is 50 { name: "revertMaxRevisions", type: "integer" }, // twinklebatchdelete.js: How many pages should be processed at a time { name: "batchdeleteChunks", type: "integer" }, // twinklebatchdelete.js: How many pages left in the process of being completed should allow a new batch to be initialized { name: "batchDeleteMinCutOff", type: "integer" }, // twinklebatchdelete.js: How many pages should be processed maximum { name: "batchMax", type: "integer" }, // twinklebatchprotect.js: How many pages should be processed at a time { name: "batchProtectChunks", type: "integer" }, // twinklebatchprotect.js: How many pages left in the process of being completed should allow a new batch to be initialized { name: "batchProtectMinCutOff", type: "integer" }, // twinklebatchundelete.js: How many pages should be processed at a time { name: "batchundeleteChunks", type: "integer" }, // twinklebatchundelete.js: How many pages left in the process of being completed should allow a new batch to be initialized { name: "batchUndeleteMinCutOff", type: "integer" } ] } ]; // end of Twinkle.config.sections //{ // name: "", // label: "", // type: "" // }, Twinkle.config.init = function twinkleconfigInit() { if (( mw.config.get("wgTitle") === "Twinkle/Preferences" || (mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user && mw.config.get("wgTitle").lastIndexOf("/Twinkle preferences") === (mw.config.get("wgTitle").length - 20))) && mw.config.get("wgAction") === "view") { // create the config page at Wikipedia:Twinkle/Preferences, and at user subpages (for testing purposes) if (!document.getElementById("twinkle-config")) { return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless } // set style (the url() CSS function doesn't seem to work from wikicode - ?!) document.getElementById("twinkle-config-titlebar").style.backgroundImage = "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC)"; var contentdiv = document.getElementById("twinkle-config-content"); contentdiv.textContent = ""; // clear children // let user know about possible conflict with monobook.js/vector.js file // (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings) var contentnotice = document.createElement("p"); // I hate innerHTML, but this is one thing it *is* good for... contentnotice.innerHTML = "<b>Before modifying your preferences here,</b> make sure you have removed any old <code>TwinkleConfig</code> and <code>FriendlyConfig</code> settings from your <a href=\"" + mw.util.getUrl("Special:MyPage/skin.js") + "\" title=\"Special:MyPage/skin.js\">user JavaScript file</a>."; contentdiv.appendChild(contentnotice); // look and see if the user does in fact have any old settings in their skin JS file var skinjs = new Morebits.wiki.page("User:" + mw.config.get("wgUserName") + "/" + mw.config.get("skin") + ".js"); skinjs.setCallbackParameters(contentnotice); skinjs.load(Twinkle.config.legacyPrefsNotice); // start a table of contents var toctable = document.createElement("table"); toctable.className = "toc"; toctable.style.marginLeft = "0.4em"; var toctr = document.createElement("tr"); var toctd = document.createElement("td"); // create TOC title var toctitle = document.createElement("div"); toctitle.id = "toctitle"; var toch2 = document.createElement("h2"); toch2.textContent = "Contents "; toctitle.appendChild(toch2); // add TOC show/hide link var toctoggle = document.createElement("span"); toctoggle.className = "toctoggle"; toctoggle.appendChild(document.createTextNode("[")); var toctogglelink = document.createElement("a"); toctogglelink.className = "internal"; toctogglelink.setAttribute("href", "#tw-tocshowhide"); toctogglelink.textContent = "hide"; toctoggle.appendChild(toctogglelink); toctoggle.appendChild(document.createTextNode("]")); toctitle.appendChild(toctoggle); toctd.appendChild(toctitle); // create item container: this is what we add stuff to var tocul = document.createElement("ul"); toctogglelink.addEventListener("click", function twinkleconfigTocToggle() { var $tocul = $(tocul); $tocul.toggle(); if ($tocul.find(":visible").length) { toctogglelink.textContent = "hide"; } else { toctogglelink.textContent = "show"; } }, false); toctd.appendChild(tocul); toctr.appendChild(toctd); toctable.appendChild(toctr); contentdiv.appendChild(toctable); var tocnumber = 1; var contentform = document.createElement("form"); contentform.setAttribute("action", "javascript:void(0)"); // was #tw-save - changed to void(0) to work around Chrome issue contentform.addEventListener("submit", Twinkle.config.save, true); contentdiv.appendChild(contentform); var container = document.createElement("table"); container.style.width = "100%"; contentform.appendChild(container); $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) { return true; // i.e. "continue" in this context } var configgetter; // retrieve the live config values if (section.inFriendlyConfig) { configgetter = Twinkle.getFriendlyPref; } else { configgetter = Twinkle.getPref; } // add to TOC var tocli = document.createElement("li"); tocli.className = "toclevel-1"; var toca = document.createElement("a"); toca.setAttribute("href", "#twinkle-config-section-" + tocnumber.toString()); toca.appendChild(document.createTextNode(section.title)); tocli.appendChild(toca); tocul.appendChild(tocli); var row = document.createElement("tr"); var cell = document.createElement("td"); cell.setAttribute("colspan", "3"); var heading = document.createElement("h4"); heading.style.borderBottom = "1px solid gray"; heading.style.marginTop = "0.2em"; heading.id = "twinkle-config-section-" + (tocnumber++).toString(); heading.appendChild(document.createTextNode(section.title)); cell.appendChild(heading); row.appendChild(cell); container.appendChild(row); var rowcount = 1; // for row banding // add each of the preferences to the form $(section.preferences).each(function(prefkey, pref) { if (pref.adminOnly && !Morebits.userIsInGroup("sysop")) { return true; // i.e. "continue" in this context } row = document.createElement("tr"); row.style.marginBottom = "0.2em"; // create odd row banding if (rowcount++ % 2 === 0) { row.style.backgroundColor = "rgba(128, 128, 128, 0.1)"; } cell = document.createElement("td"); var label, input; switch (pref.type) { case "boolean": // create a checkbox cell.setAttribute("colspan", "2"); label = document.createElement("label"); input = document.createElement("input"); input.setAttribute("type", "checkbox"); input.setAttribute("id", pref.name); input.setAttribute("name", pref.name); if (configgetter(pref.name) === true) { input.setAttribute("checked", "checked"); } label.appendChild(input); label.appendChild(document.createTextNode(" " + pref.label)); cell.appendChild(label); break; case "string": // create an input box case "integer": // add label to first column cell.style.textAlign = "right"; cell.style.paddingRight = "0.5em"; label = document.createElement("label"); label.setAttribute("for", pref.name); label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); row.appendChild(cell); // add input box to second column cell = document.createElement("td"); cell.style.paddingRight = "1em"; input = document.createElement("input"); input.setAttribute("type", "text"); input.setAttribute("id", pref.name); input.setAttribute("name", pref.name); if (pref.type === "integer") { input.setAttribute("size", 6); input.setAttribute("type", "number"); input.setAttribute("step", "1"); // integers only } if (configgetter(pref.name)) { input.setAttribute("value", configgetter(pref.name)); } cell.appendChild(input); break; case "enum": // create a combo box // add label to first column // note: duplicates the code above, under string/integer cell.style.textAlign = "right"; cell.style.paddingRight = "0.5em"; label = document.createElement("label"); label.setAttribute("for", pref.name); label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); row.appendChild(cell); // add input box to second column cell = document.createElement("td"); cell.style.paddingRight = "1em"; input = document.createElement("select"); input.setAttribute("id", pref.name); input.setAttribute("name", pref.name); $.each(pref.enumValues, function(enumvalue, enumdisplay) { var option = document.createElement("option"); option.setAttribute("value", enumvalue); if (configgetter(pref.name) === enumvalue) { option.setAttribute("selected", "selected"); } option.appendChild(document.createTextNode(enumdisplay)); input.appendChild(option); }); cell.appendChild(input); break; case "set": // create a set of check boxes // add label first of all cell.setAttribute("colspan", "2"); label = document.createElement("label"); // not really necessary to use a label element here, but we do it for consistency of styling label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); var checkdiv = document.createElement("div"); checkdiv.style.paddingLeft = "1em"; var worker = function(itemkey, itemvalue) { var checklabel = document.createElement("label"); checklabel.style.marginRight = "0.7em"; checklabel.style.display = "inline-block"; var check = document.createElement("input"); check.setAttribute("type", "checkbox"); check.setAttribute("id", pref.name + "_" + itemkey); check.setAttribute("name", pref.name + "_" + itemkey); if (configgetter(pref.name) && configgetter(pref.name).indexOf(itemkey) !== -1) { check.setAttribute("checked", "checked"); } // cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...) if (pref.name === "unlinkNamespaces") { if (configgetter(pref.name) && configgetter(pref.name).indexOf(parseInt(itemkey, 10)) !== -1) { check.setAttribute("checked", "checked"); } } checklabel.appendChild(check); checklabel.appendChild(document.createTextNode(itemvalue)); checkdiv.appendChild(checklabel); }; if (pref.setDisplayOrder) { // add check boxes according to the given display order $.each(pref.setDisplayOrder, function(itemkey, item) { worker(item, pref.setValues[item]); }); } else { // add check boxes according to the order it gets fed to us (probably strict alphabetical) $.each(pref.setValues, worker); } cell.appendChild(checkdiv); break; case "customList": // add label to first column cell.style.textAlign = "right"; cell.style.paddingRight = "0.5em"; label = document.createElement("label"); label.setAttribute("for", pref.name); label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); row.appendChild(cell); // add button to second column cell = document.createElement("td"); cell.style.paddingRight = "1em"; var button = document.createElement("button"); button.setAttribute("id", pref.name); button.setAttribute("name", pref.name); button.setAttribute("type", "button"); button.addEventListener("click", Twinkle.config.listDialog.display, false); // use jQuery data on the button to store the current config value $(button).data({ value: configgetter(pref.name), pref: pref, inFriendlyConfig: section.inFriendlyConfig }); button.appendChild(document.createTextNode("Edit items")); cell.appendChild(button); break; default: alert("twinkleconfig: unknown data type for preference " + pref.name); break; } row.appendChild(cell); // add help tip cell = document.createElement("td"); cell.style.fontSize = "90%"; cell.style.color = "gray"; if (pref.helptip) { cell.innerHTML = pref.helptip; } // add reset link (custom lists don't need this, as their config value isn't displayed on the form) if (pref.type !== "customList") { var resetlink = document.createElement("a"); resetlink.setAttribute("href", "#tw-reset"); resetlink.setAttribute("id", "twinkle-config-reset-" + pref.name); resetlink.addEventListener("click", Twinkle.config.resetPrefLink, false); if (resetlink.style.styleFloat) { // IE (inc. IE9) resetlink.style.styleFloat = "right"; } else { // standards resetlink.style.cssFloat = "right"; } resetlink.style.margin = "0 0.6em"; resetlink.appendChild(document.createTextNode("Reset")); cell.appendChild(resetlink); } row.appendChild(cell); container.appendChild(row); return true; }); return true; }); var footerbox = document.createElement("div"); footerbox.setAttribute("id", "twinkle-config-buttonpane"); footerbox.style.backgroundColor = "#BCCADF"; footerbox.style.padding = "0.5em"; var button = document.createElement("button"); button.setAttribute("id", "twinkle-config-submit"); button.setAttribute("type", "submit"); button.appendChild(document.createTextNode("Save changes")); footerbox.appendChild(button); var footerspan = document.createElement("span"); footerspan.className = "plainlinks"; footerspan.style.marginLeft = "2.4em"; footerspan.style.fontSize = "90%"; var footera = document.createElement("a"); footera.setAttribute("href", "#tw-reset-all"); footera.setAttribute("id", "twinkle-config-resetall"); footera.addEventListener("click", Twinkle.config.resetAllPrefs, false); footera.appendChild(document.createTextNode("Restore defaults")); footerspan.appendChild(footera); footerbox.appendChild(footerspan); contentform.appendChild(footerbox); // since all the section headers exist now, we can try going to the requested anchor if (location.hash) { location.hash = location.hash; } } else if (mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user) { var box = document.createElement("div"); box.setAttribute("id", "twinkle-config-headerbox"); box.style.border = "1px #f60 solid"; box.style.background = "#fed"; box.style.padding = "0.6em"; box.style.margin = "0.5em auto"; box.style.textAlign = "center"; var link; if (mw.config.get("wgTitle") === mw.config.get("wgUserName") + "/twinkleoptions.js") { // place "why not try the preference panel" notice box.style.fontWeight = "bold"; box.style.width = "80%"; box.style.borderWidth = "2px"; if (mw.config.get("wgArticleId") > 0) { // page exists box.appendChild(document.createTextNode("This page contains your Twinkle preferences. You can change them using the ")); } else { // page does not exist box.appendChild(document.createTextNode("You can customize Twinkle to suit your preferences by using the ")); } link = document.createElement("a"); link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") ); link.appendChild(document.createTextNode("Twinkle preferences panel")); box.appendChild(link); box.appendChild(document.createTextNode(", or by editing this page.")); $(box).insertAfter($("#contentSub")); } else if (mw.config.get("wgTitle").indexOf(mw.config.get("wgUserName")) === 0 && mw.config.get("wgPageName").lastIndexOf(".js") === mw.config.get("wgPageName").length - 3) { // place "Looking for Twinkle options?" notice box.style.width = "60%"; box.appendChild(document.createTextNode("If you want to set Twinkle preferences, you can use the ")); link = document.createElement("a"); link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") ); link.appendChild(document.createTextNode("Twinkle preferences panel")); box.appendChild(link); box.appendChild(document.createTextNode(".")); $(box).insertAfter($("#contentSub")); } } }; // Morebits.wiki.page callback from init code Twinkle.config.legacyPrefsNotice = function twinkleconfigLegacyPrefsNotice(pageobj) { var text = pageobj.getPageText(); var contentnotice = pageobj.getCallbackParameters(); if (text.indexOf("TwinkleConfig") !== -1 || text.indexOf("FriendlyConfig") !== -1) { contentnotice.innerHTML = '<table class="plainlinks ombox ombox-content"><tr><td class="mbox-image">' + '<img alt="" src="http://upload.wikimedia.org/wikipedia/en/3/38/Imbox_content.png" /></td>' + '<td class="mbox-text"><p><big><b>Before modifying your settings here,</b> you must remove your old Twinkle and Friendly settings from your personal skin JavaScript.</big></p>' + '<p>To do this, you can <a href="' + mw.config.get("wgScript") + '?title=User:' + encodeURIComponent(mw.config.get("wgUserName")) + '/' + mw.config.get("skin") + '.js&action=edit" target="_tab"><b>edit your personal JavaScript</b></a>, removing all lines of code that refer to <code>TwinkleConfig</code> and <code>FriendlyConfig</code>.</p>' + '</td></tr></table>'; } else { $(contentnotice).remove(); } }; // custom list-related stuff Twinkle.config.listDialog = {}; Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow(dlgtable, value, label) { var contenttr = document.createElement("tr"); // "remove" button var contenttd = document.createElement("td"); var removeButton = document.createElement("button"); removeButton.setAttribute("type", "button"); removeButton.addEventListener("click", function() { $(contenttr).remove(); }, false); removeButton.textContent = "Remove"; contenttd.appendChild(removeButton); contenttr.appendChild(contenttd); // value input box contenttd = document.createElement("td"); var input = document.createElement("input"); input.setAttribute("type", "text"); input.className = "twinkle-config-customlist-value"; input.style.width = "97%"; if (value) { input.setAttribute("value", value); } contenttd.appendChild(input); contenttr.appendChild(contenttd); // label input box contenttd = document.createElement("td"); input = document.createElement("input"); input.setAttribute("type", "text"); input.className = "twinkle-config-customlist-label"; input.style.width = "98%"; if (label) { input.setAttribute("value", label); } contenttd.appendChild(input); contenttr.appendChild(contenttd); dlgtable.appendChild(contenttr); }; Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) { var $prefbutton = $(e.target); var curvalue = $prefbutton.data("value"); var curpref = $prefbutton.data("pref"); var dialog = new Morebits.simpleWindow(720, 400); dialog.setTitle(curpref.label); dialog.setScriptName("Twinkle preferences"); var dialogcontent = document.createElement("div"); var dlgtable = document.createElement("table"); dlgtable.className = "wikitable"; dlgtable.style.margin = "1.4em 1em"; dlgtable.style.width = "auto"; var dlgtbody = document.createElement("tbody"); // header row var dlgtr = document.createElement("tr"); // top-left cell var dlgth = document.createElement("th"); dlgth.style.width = "5%"; dlgtr.appendChild(dlgth); // value column header dlgth = document.createElement("th"); dlgth.style.width = "35%"; dlgth.textContent = (curpref.customListValueTitle ? curpref.customListValueTitle : "Value"); dlgtr.appendChild(dlgth); // label column header dlgth = document.createElement("th"); dlgth.style.width = "60%"; dlgth.textContent = (curpref.customListLabelTitle ? curpref.customListLabelTitle : "Label"); dlgtr.appendChild(dlgth); dlgtbody.appendChild(dlgtr); // content rows var gotRow = false; $.each(curvalue, function(k, v) { gotRow = true; Twinkle.config.listDialog.addRow(dlgtbody, v.value, v.label); }); // if there are no values present, add a blank row to start the user off if (!gotRow) { Twinkle.config.listDialog.addRow(dlgtbody); } // final "add" button var dlgtfoot = document.createElement("tfoot"); dlgtr = document.createElement("tr"); var dlgtd = document.createElement("td"); dlgtd.setAttribute("colspan", "3"); var addButton = document.createElement("button"); addButton.style.minWidth = "8em"; addButton.setAttribute("type", "button"); addButton.addEventListener("click", function(e) { Twinkle.config.listDialog.addRow(dlgtbody); }, false); addButton.textContent = "Add"; dlgtd.appendChild(addButton); dlgtr.appendChild(dlgtd); dlgtfoot.appendChild(dlgtr); dlgtable.appendChild(dlgtbody); dlgtable.appendChild(dlgtfoot); dialogcontent.appendChild(dlgtable); // buttonpane buttons: [Save changes] [Reset] [Cancel] var button = document.createElement("button"); button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane button.addEventListener("click", function(e) { Twinkle.config.listDialog.save($prefbutton, dlgtbody); dialog.close(); }, false); button.textContent = "Save changes"; dialogcontent.appendChild(button); button = document.createElement("button"); button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane button.addEventListener("click", function(e) { Twinkle.config.listDialog.reset($prefbutton, dlgtbody); }, false); button.textContent = "Reset"; dialogcontent.appendChild(button); button = document.createElement("button"); button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane button.addEventListener("click", function(e) { dialog.close(); // the event parameter on this function seems to be broken }, false); button.textContent = "Cancel"; dialogcontent.appendChild(button); dialog.setContent(dialogcontent); dialog.display(); }; // Resets the data value, re-populates based on the new (default) value, then saves the // old data value again (less surprising behaviour) Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset(button, tbody) { // reset value on button var $button = $(button); var curpref = $button.data("pref"); var oldvalue = $button.data("value"); Twinkle.config.resetPref(curpref, $button.data("inFriendlyConfig")); // reset form var $tbody = $(tbody); $tbody.find("tr").slice(1).remove(); // all rows except the first (header) row // add the new values var curvalue = $button.data("value"); $.each(curvalue, function(k, v) { Twinkle.config.listDialog.addRow(tbody, v.value, v.label); }); // save the old value $button.data("value", oldvalue); }; Twinkle.config.listDialog.save = function twinkleconfigListDialogSave(button, tbody) { var result = []; var current = {}; $(tbody).find('input[type="text"]').each(function(inputkey, input) { if ($(input).hasClass("twinkle-config-customlist-value")) { current = { value: input.value }; } else { current.label = input.value; // exclude totally empty rows if (current.value || current.label) { result.push(current); } } }); $(button).data("value", result); }; // reset/restore defaults Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) { var wantedpref = e.target.id.substring(21); // "twinkle-config-reset-" prefix is stripped // search tactics $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) { return true; // continue: skip impossibilities } var foundit = false; $(section.preferences).each(function(prefkey, pref) { if (pref.name !== wantedpref) { return true; // continue } Twinkle.config.resetPref(pref, section.inFriendlyConfig); foundit = true; return false; // break }); if (foundit) { return false; // break } }); return false; // stop link from scrolling page }; Twinkle.config.resetPref = function twinkleconfigResetPref(pref, inFriendlyConfig) { switch (pref.type) { case "boolean": document.getElementById(pref.name).checked = (inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]); break; case "string": case "integer": case "enum": document.getElementById(pref.name).value = (inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]); break; case "set": $.each(pref.setValues, function(itemkey, itemvalue) { if (document.getElementById(pref.name + "_" + itemkey)) { document.getElementById(pref.name + "_" + itemkey).checked = ((inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]).indexOf(itemkey) !== -1); } }); break; case "customList": $(document.getElementById(pref.name)).data("value", (inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name])); break; default: alert("twinkleconfig: unknown data type for preference " + pref.name); break; } }; Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() { // no confirmation message - the user can just refresh/close the page to abort $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) { return true; // continue: skip impossibilities } $(section.preferences).each(function(prefkey, pref) { if (!pref.adminOnly || Morebits.userIsInGroup("sysop")) { Twinkle.config.resetPref(pref, section.inFriendlyConfig); } }); return true; }); return false; // stop link from scrolling page }; Twinkle.config.save = function twinkleconfigSave(e) { Morebits.status.init( document.getElementById("twinkle-config-content") ); Morebits.wiki.actionCompleted.notice = "Save"; var userjs = mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").user] + ":" + mw.config.get("wgUserName") + "/twinkleoptions.js"; var wikipedia_page = new Morebits.wiki.page(userjs, "Saving preferences to " + userjs); wikipedia_page.setCallbackParameters(e.target); wikipedia_page.load(Twinkle.config.writePrefs); return false; }; Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) { var form = pageobj.getCallbackParameters(); var statelem = pageobj.getStatusElement(); // this is the object which gets serialized into JSON var newConfig = { twinkle: {}, friendly: {} }; // keeping track of all preferences that we encounter // any others that are set in the user's current config are kept // this way, preferences that this script doesn't know about are not lost // (it does mean obsolete prefs will never go away, but... ah well...) var foundTwinklePrefs = [], foundFriendlyPrefs = []; // a comparison function is needed later on // it is just enough for our purposes (i.e. comparing strings, numbers, booleans, // arrays of strings, and arrays of { value, label }) // and it is not very robust: e.g. compare([2], ["2"]) === true, and // compare({}, {}) === false, but it's good enough for our purposes here var compare = function(a, b) { if ($.isArray(a)) { if (a.length !== b.length) { return false; } var asort = a.sort(), bsort = b.sort(); for (var i = 0; asort[i]; ++i) { // comparison of the two properties of custom lists if ((typeof asort[i] === "object") && (asort[i].label !== bsort[i].label || asort[i].value !== bsort[i].value)) { return false; } else if (asort[i].toString() !== bsort[i].toString()) { return false; } } return true; } else { return a === b; } }; $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.adminOnly && !Morebits.userIsInGroup("sysop")) { return; // i.e. "continue" in this context } // reach each of the preferences from the form $(section.preferences).each(function(prefkey, pref) { var userValue; // = undefined // only read form values for those prefs that have them if (!section.hidden && (!pref.adminOnly || Morebits.userIsInGroup("sysop"))) { switch (pref.type) { case "boolean": // read from the checkbox userValue = form[pref.name].checked; break; case "string": // read from the input box or combo box case "enum": userValue = form[pref.name].value; break; case "integer": // read from the input box userValue = parseInt(form[pref.name].value, 10); if (isNaN(userValue)) { Morebits.status.warn("Saving", "The value you specified for " + pref.name + " (" + pref.value + ") was invalid. The save will continue, but the invalid data value will be skipped."); userValue = null; } break; case "set": // read from the set of check boxes userValue = []; if (pref.setDisplayOrder) { // read only those keys specified in the display order $.each(pref.setDisplayOrder, function(itemkey, item) { if (form[pref.name + "_" + item].checked) { userValue.push(item); } }); } else { // read all the keys in the list of values $.each(pref.setValues, function(itemkey, itemvalue) { if (form[pref.name + "_" + itemkey].checked) { userValue.push(itemkey); } }); } break; case "customList": // read from the jQuery data stored on the button object userValue = $(form[pref.name]).data("value"); break; default: alert("twinkleconfig: unknown data type for preference " + pref.name); break; } } // only save those preferences that are *different* from the default if (section.inFriendlyConfig) { if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.friendly[pref.name])) { newConfig.friendly[pref.name] = userValue; } foundFriendlyPrefs.push(pref.name); } else { if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.twinkle[pref.name])) { newConfig.twinkle[pref.name] = userValue; } foundTwinklePrefs.push(pref.name); } }); }); if (Twinkle.prefs) { $.each(Twinkle.prefs.twinkle, function(tkey, tvalue) { if (foundTwinklePrefs.indexOf(tkey) === -1) { newConfig.twinkle[tkey] = tvalue; } }); $.each(Twinkle.prefs.friendly, function(fkey, fvalue) { if (foundFriendlyPrefs.indexOf(fkey) === -1) { newConfig.friendly[fkey] = fvalue; } }); } var text = "// twinkleoptions.js: personal Twinkle preferences file\n" + "//\n" + "// NOTE: The easiest way to change your Twinkle preferences is by using the\n" + "// Twinkle preferences panel, at [[" + mw.config.get("wgPageName") + "]].\n" + "//\n" + "// This file is AUTOMATICALLY GENERATED. Any changes you make (aside from\n" + "// changing the configuration parameters in a valid-JavaScript way) will be\n" + "// overwritten the next time you click \"save\" in the Twinkle preferences\n" + "// panel. If modifying this file, make sure to use correct JavaScript.\n" + "\n" + "window.Twinkle.prefs = "; text += JSON.stringify(newConfig, null, 2); text += ";\n" + "\n" + "// End of twinkleoptions.js\n"; pageobj.setPageText(text); pageobj.setEditSummary("Saving Twinkle preferences: automatic edit from [[" + mw.config.get("wgPageName") + "]]"); pageobj.setCreateOption("recreate"); pageobj.save(Twinkle.config.saveSuccess); }; Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) { pageobj.getStatusElement().info("successful"); var noticebox = document.createElement("div"); noticebox.className = "successbox"; noticebox.style.fontSize = "100%"; noticebox.style.marginTop = "2em"; noticebox.innerHTML = "<p><b>Your Twinkle preferences have been saved.</b></p><p>To see the changes, you will need to <b>clear your browser cache entirely</b> (see <a href=\"" + mw.util.getUrl("WP:BYPASS") + "\" title=\"WP:BYPASS\">WP:BYPASS</a> for instructions).</p>"; Morebits.status.root.appendChild(noticebox); var noticeclear = document.createElement("br"); noticeclear.style.clear = "both"; Morebits.status.root.appendChild(noticeclear); }; /* **************************************** *** twinklediff.js: Diff module **************************************** * Mode of invocation: Tab on non-diff pages ("Last"); tabs on diff pages ("Since", "Since mine", "Current") * Active on: Existing non-special pages * Config directives in: TwinkleConfig */ Twinkle.diff = function twinklediff() { if( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') ) { return; } var query = { 'title': mw.config.get('wgPageName'), 'diff': 'cur', 'oldid': 'prev' }; twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Last', 'tw-lastdiff', 'Show most recent diff' ); // Show additional tabs only on diff pages if (Morebits.queryString.exists('diff')) { twAddPortletLink(function(){ Twinkle.diff.evaluate(false); }, 'Since', 'tw-since', 'Show difference between last diff and the revision made by previous user' ); twAddPortletLink( function(){ Twinkle.diff.evaluate(true); }, 'Since mine', 'tw-sincemine', 'Show difference between last diff and my last revision' ); var oldid = /oldid=(.+)/.exec($('#mw-diff-ntitle1').find('strong a').first().attr("href"))[1]; query = { 'title': mw.config.get('wgPageName'), 'diff': 'cur', 'oldid' : oldid }; twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Current', 'tw-curdiff', 'Show difference to current revision' ); } }; Twinkle.diff.evaluate = function twinklediffEvaluate(me) { var user; if( me ) { user = mw.config.get('wgUserName'); } else { var node = document.getElementById( 'mw-diff-ntitle2' ); if( ! node ) { // nothing to do? return; } user = $(node).find('a').first().text(); } var query = { 'prop': 'revisions', 'action': 'query', 'titles': mw.config.get('wgPageName'), 'rvlimit': 1, 'rvprop': [ 'ids', 'user' ], 'rvstartid': mw.config.get('wgCurRevisionId') - 1, // i.e. not the current one 'rvuser': user }; Morebits.status.init( document.getElementById('bodyContent') ); var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of initial contributor', query, Twinkle.diff.callbacks.main ); wikipedia_api.params = { user: user }; wikipedia_api.post(); }; Twinkle.diff.callbacks = { main: function( self ) { var xmlDoc = self.responseXML; var revid = $(xmlDoc).find('rev').attr('revid'); if( ! revid ) { self.statelem.error( 'no suitable earlier revision found, or ' + self.params.user + ' is the only contributor. Aborting.' ); return; } var query = { 'title': mw.config.get('wgPageName'), 'oldid': revid, 'diff': mw.config.get('wgCurRevisionId') }; window.location = mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ); } }; /* **************************************** *** twinklefluff.js: Revert/rollback module **************************************** * Mode of invocation: Links on history, contributions, and diff pages * Active on: Diff pages, history pages, contributions pages * Config directives in: TwinkleConfig */ /** Twinklefluff revert and antivandalism utility */ Twinkle.fluff = { auto: function() { if( parseInt( Morebits.queryString.get('oldid'), 10) !== mw.config.get('wgCurRevisionId') ) { // not latest revision alert("Can't rollback, page has changed in the meantime."); return; } var vandal = $("#mw-diff-ntitle2").find("a.mw-userlink").text(); Twinkle.fluff.revert( Morebits.queryString.get( 'twinklerevert' ), vandal, true ); }, normal: function() { var spanTag = function( color, content ) { var span = document.createElement( 'span' ); span.style.color = color; span.appendChild( document.createTextNode( content ) ); return span; }; if( mw.config.get('wgNamespaceNumber') === -1 && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) { //Get the username these contributions are for var lastLogNode = $('#contentSub').find('a[title^="Special:Log"]').last(); if(!lastLogNode) return; var logMatch = /wiki\/Special:Log\/(.+)$/.exec( lastLogNode ? lastLogNode.attr("href").replace(/_/g, "%20") : '' ); if(!logMatch) return; username = decodeURIComponent(logMatch[1]); if( Twinkle.getPref('showRollbackLinks').indexOf('contribs') !== -1 || ( mw.config.get('wgUserName') !== username && Twinkle.getPref('showRollbackLinks').indexOf('others') !== -1 ) || ( mw.config.get('wgUserName') === username && Twinkle.getPref('showRollbackLinks').indexOf('mine') !== -1 ) ) { var list = $("#bodyContent").find("ul li:has(span.mw-uctop)"); var revNode = document.createElement('strong'); var revLink = document.createElement('a'); revLink.appendChild( spanTag( 'Black', '[' ) ); revLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) ); revLink.appendChild( spanTag( 'Black', ']' ) ); revNode.appendChild(revLink); var revVandNode = document.createElement('strong'); var revVandLink = document.createElement('a'); revVandLink.appendChild( spanTag( 'Black', '[' ) ); revVandLink.appendChild( spanTag( 'Red', 'vandalism' ) ); revVandLink.appendChild( spanTag( 'Black', ']' ) ); revVandNode.appendChild(revVandLink); list.each(function(key, current) { var href = $(current).children("a:eq(1)").attr("href"); current.appendChild( document.createTextNode(' ') ); var tmpNode = revNode.cloneNode( true ); tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'norm' } ) ); current.appendChild( tmpNode ); current.appendChild( document.createTextNode(' ') ); tmpNode = revVandNode.cloneNode( true ); tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'vand' } ) ); current.appendChild( tmpNode ); }); } } else { if( mw.config.get('wgCanonicalSpecialPageName') === "Undelete" ) { //You can't rollback deleted pages! return; } var body = document.getElementById('bodyContent'); var firstRev = $("div.firstrevisionheader").length; if( firstRev ) { // we have first revision here, nothing to do. return; } var otitle, ntitle; try { var otitle1 = document.getElementById('mw-diff-otitle1'); var ntitle1 = document.getElementById('mw-diff-ntitle1'); if (!otitle1 || !ntitle1) { return; } otitle = otitle1.parentNode; ntitle = ntitle1.parentNode; } catch( e ) { // no old, nor new title, nothing to do really, return; return; } var old_rev_url = $("#mw-diff-otitle1").find("strong a").attr("href"); // Lets first add a [edit this revision] link var query = new Morebits.queryString( old_rev_url.split( '?', 2 )[1] ); var oldrev = query.get('oldid'); var revertToRevision = document.createElement('div'); revertToRevision.setAttribute( 'id', 'tw-revert-to-orevision' ); revertToRevision.style.fontWeight = 'bold'; var revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') ); revertToRevisionLink.href = "#"; $(revertToRevisionLink).click(function(){ Twinkle.fluff.revertToRevision(oldrev); }); revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) ); revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) ); revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) ); otitle.insertBefore( revertToRevision, otitle.firstChild ); if( document.getElementById('differences-nextlink') ) { // Not latest revision curVersion = false; var new_rev_url = $("#mw-diff-ntitle1").find("strong a").attr("href"); query = new Morebits.queryString( new_rev_url.split( '?', 2 )[1] ); var newrev = query.get('oldid'); revertToRevision = document.createElement('div'); revertToRevision.setAttribute( 'id', 'tw-revert-to-nrevision' ); revertToRevision.style.fontWeight = 'bold'; revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') ); revertToRevisionLink.href = "#"; $(revertToRevisionLink).click(function(){ Twinkle.fluff.revertToRevision(newrev); }); revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) ); revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) ); revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) ); ntitle.insertBefore( revertToRevision, ntitle.firstChild ); return; } if( Twinkle.getPref('showRollbackLinks').indexOf('diff') !== -1 ) { var vandal = $("#mw-diff-ntitle2").find("a").first().text(); var revertNode = document.createElement('div'); revertNode.setAttribute( 'id', 'tw-revert' ); var agfNode = document.createElement('strong'); var vandNode = document.createElement('strong'); var normNode = document.createElement('strong'); var agfLink = document.createElement('a'); var vandLink = document.createElement('a'); var normLink = document.createElement('a'); agfLink.href = "#"; vandLink.href = "#"; normLink.href = "#"; $(agfLink).click(function(){ Twinkle.fluff.revert('agf', vandal); }); $(vandLink).click(function(){ Twinkle.fluff.revert('vand', vandal); }); $(normLink).click(function(){ Twinkle.fluff.revert('norm', vandal); }); agfLink.appendChild( spanTag( 'Black', '[' ) ); agfLink.appendChild( spanTag( 'DarkOliveGreen', 'rollback (AGF)' ) ); agfLink.appendChild( spanTag( 'Black', ']' ) ); vandLink.appendChild( spanTag( 'Black', '[' ) ); vandLink.appendChild( spanTag( 'Red', 'rollback (VANDAL)' ) ); vandLink.appendChild( spanTag( 'Black', ']' ) ); normLink.appendChild( spanTag( 'Black', '[' ) ); normLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) ); normLink.appendChild( spanTag( 'Black', ']' ) ); agfNode.appendChild(agfLink); vandNode.appendChild(vandLink); normNode.appendChild(normLink); revertNode.appendChild( agfNode ); revertNode.appendChild( document.createTextNode(' || ') ); revertNode.appendChild( normNode ); revertNode.appendChild( document.createTextNode(' || ') ); revertNode.appendChild( vandNode ); ntitle.insertBefore( revertNode, ntitle.firstChild ); } } } }; Twinkle.fluff.revert = function revertPage( type, vandal, autoRevert, rev, page ) { if (mw.util.isIPv6Address(vandal)) { vandal = Morebits.sanitizeIPv6(vandal); } var pagename = page || mw.config.get('wgPageName'); var revid = rev || mw.config.get('wgCurRevisionId'); Morebits.status.init( document.getElementById('bodyContent') ); var params = { type: type, user: vandal, pagename: pagename, revid: revid, autoRevert: !!autoRevert }; var query = { 'action': 'query', 'prop': ['info', 'revisions'], 'titles': pagename, 'rvlimit': 50, // max possible 'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ], 'curtimestamp': '', 'meta': 'tokens', 'type': 'csrf', }; var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of earlier revisions', query, Twinkle.fluff.callbacks.main ); wikipedia_api.params = params; wikipedia_api.post(); }; Twinkle.fluff.revertToRevision = function revertToRevision( oldrev ) { Morebits.status.init( document.getElementById('bodyContent') ); var query = { 'action': 'query', 'prop': ['info', 'revisions'], 'titles': mw.config.get('wgPageName'), 'rvlimit': 1, 'rvstartid': oldrev, 'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ], 'curtimestamp': '', 'meta': 'tokens', 'type': 'csrf', 'format': 'xml' }; var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of the earlier revision', query, Twinkle.fluff.callbacks.toRevision.main ); wikipedia_api.params = { rev: oldrev }; wikipedia_api.post(); }; Twinkle.fluff.userIpLink = function( user ) { return (Morebits.isIPAddress(user) ? "[[Special:Contributions/" : "[[User:" ) + user + "|" + user + "]]"; }; Twinkle.fluff.callbacks = { toRevision: { main: function( self ) { var xmlDoc = self.responseXML; var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10); var touched = $(xmlDoc).find('page').attr('touched'); var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp'); var edittoken = $(xmlDoc).find('tokens').attr('csrftoken'); var revertToRevID = $(xmlDoc).find('rev').attr('revid'); var revertToUser = $(xmlDoc).find('rev').attr('user'); if (revertToRevID !== self.params.rev) { self.statitem.error( 'The retrieved revision does not match the requested revision. Aborting.' ); return; } var optional_summary = prompt( "Please specify a reason for the revert: ", "" ); // padded out to widen prompt in Firefox if (optional_summary === null) { self.statelem.error( 'Aborted by user.' ); return; } var summary = "Reverted to revision " + revertToRevID + " by " + revertToUser + (optional_summary ? ": " + optional_summary : '') + "." + Twinkle.getPref('summaryAd'); var query = { 'action': 'edit', 'title': mw.config.get('wgPageName'), 'summary': summary, 'token': edittoken, 'undo': lastrevid, 'undoafter': revertToRevID, 'basetimestamp': touched, 'starttimestamp': starttimestamp, 'watchlist': Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined, 'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined }; Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Reversion completed"; var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, null/*Twinkle.fluff.callbacks.toRevision.complete*/, self.statelem); wikipedia_api.params = self.params; wikipedia_api.post(); }, complete: function (self) { } }, main: function( self ) { var xmlDoc = self.responseXML; var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10); var touched = $(xmlDoc).find('page').attr('touched'); var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp'); var edittoken = $(xmlDoc).find('tokens').attr('csrftoken'); var lastuser = $(xmlDoc).find('rev').attr('user'); var revs = $(xmlDoc).find('rev'); if( revs.length < 1 ) { self.statelem.error( 'We have less than one additional revision, thus impossible to revert' ); return; } var top = revs[0]; if( lastrevid < self.params.revid ) { Morebits.status.error( 'Error', [ 'The most recent revision ID received from the server, ', Morebits.htmlNode( 'strong', lastrevid ), ', is less than the ID of the displayed revision. This could indicate that the current revision has been deleted, the server is lagging, or that bad data has been received. Will stop proceeding at this point.' ] ); return; } var index = 1; if( self.params.revid !== lastrevid ) { Morebits.status.warn( 'Warning', [ 'Latest revision ', Morebits.htmlNode( 'strong', lastrevid ), ' doesn\'t equal our revision ', Morebits.htmlNode( 'strong', self.params.revid ) ] ); if( lastuser === self.params.user ) { switch( self.params.type ) { case 'vand': Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume vandalism, we continue to revert' ]); break; case 'agf': Morebits.status.warn( 'Warning', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume good faith, we stop reverting, as the problem might have been fixed.' ]); return; default: Morebits.status.warn( 'Notice', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , ', but we will stop reverting anyway.' ] ); return; } } else if(self.params.type === 'vand' && Twinkle.fluff.whiteList.indexOf( top.getAttribute( 'user' ) ) !== -1 && revs.length > 1 && revs[1].getAttribute( 'pageId' ) === self.params.revid) { Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', a trusted bot, and the revision before was made by our vandal, so we proceed with the revert.' ] ); index = 2; } else { Morebits.status.error( 'Error', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', so it might have already been reverted, stopping reverting.'] ); return; } } if( Twinkle.fluff.whiteList.indexOf( self.params.user ) !== -1 ) { switch( self.params.type ) { case 'vand': Morebits.status.info( 'Info', [ 'Vandalism revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. As this is a whitelisted bot, we assume you wanted to revert vandalism made by the previous user instead.' ] ); index = 2; vandal = revs[1].getAttribute( 'user' ); self.params.user = revs[1].getAttribute( 'user' ); break; case 'agf': Morebits.status.warn( 'Notice', [ 'Good faith revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, it makes no sense at all to revert it as a good faith edit, will stop reverting.' ] ); return; case 'norm': /* falls through */ default: var cont = confirm( 'Normal revert was chosen, but the most recent edit was made by a whitelisted bot (' + self.params.user + '). Do you want to revert the revision before instead?' ); if( cont ) { Morebits.status.info( 'Info', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, and per confirmation, we\'ll revert the previous revision instead.' ] ); index = 2; self.params.user = revs[1].getAttribute( 'user' ); } else { Morebits.status.warn( 'Notice', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, but per confirmation, revert on top revision will proceed.' ] ); } break; } } var found = false; var count = 0; for( var i = index; i < revs.length; ++i ) { ++count; if( revs[i].getAttribute( 'user' ) !== self.params.user ) { found = i; break; } } if( ! found ) { self.statelem.error( [ 'No previous revision found. Perhaps ', Morebits.htmlNode( 'strong', self.params.user ), ' is the only contributor, or that the user has made more than ' + Twinkle.getPref('revertMaxRevisions') + ' edits in a row.' ] ); return; } if( ! count ) { Morebits.status.error( 'Error', "We were to revert zero revisions. As that makes no sense, we'll stop reverting this time. It could be that the edit has already been reverted, but the revision ID was still the same." ); return; } var good_revision = revs[ found ]; var userHasAlreadyConfirmedAction = false; if (self.params.type !== 'vand' && count > 1) { if ( !confirm( self.params.user + ' has made ' + count + ' edits in a row. Are you sure you want to revert them all?') ) { Morebits.status.info( 'Notice', 'Stopping reverting per user input' ); return; } userHasAlreadyConfirmedAction = true; } self.params.count = count; self.params.goodid = good_revision.getAttribute( 'revid' ); self.params.gooduser = good_revision.getAttribute( 'user' ); self.statelem.status( [ ' revision ', Morebits.htmlNode( 'strong', self.params.goodid ), ' that was made ', Morebits.htmlNode( 'strong', count ), ' revisions ago by ', Morebits.htmlNode( 'strong', self.params.gooduser ) ] ); var summary, extra_summary, userstr, gooduserstr; switch( self.params.type ) { case 'agf': extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox if (extra_summary === null) { self.statelem.error( 'Aborted by user.' ); return; } userHasAlreadyConfirmedAction = true; userstr = self.params.user; summary = "Reverted good faith edits by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) + Twinkle.getPref('summaryAd'); break; case 'vand': userstr = self.params.user; gooduserstr = self.params.gooduser; summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]]) identified as vandalism to last revision by " + gooduserstr + "." + Twinkle.getPref('summaryAd'); break; case 'norm': /* falls through */ default: if( Twinkle.getPref('offerReasonOnNormalRevert') ) { extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox if (extra_summary === null) { self.statelem.error( 'Aborted by user.' ); return; } userHasAlreadyConfirmedAction = true; } userstr = self.params.user; summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) + Twinkle.getPref('summaryAd'); break; } if (Twinkle.getPref('confirmOnFluff') && !userHasAlreadyConfirmedAction && !confirm("Reverting page: are you sure?")) { self.statelem.error( 'Aborted by user.' ); return; } var query; if( (!self.params.autoRevert || Twinkle.getPref('openTalkPageOnAutoRevert')) && Twinkle.getPref('openTalkPage').indexOf( self.params.type ) !== -1 && mw.config.get('wgUserName') !== self.params.user ) { Morebits.status.info( 'Info', [ 'Opening user talk page edit form for user ', Morebits.htmlNode( 'strong', self.params.user ) ] ); query = { 'title': 'User talk:' + self.params.user, 'action': 'edit', 'preview': 'yes', 'vanarticle': self.params.pagename.replace(/_/g, ' '), 'vanarticlerevid': self.params.revid, 'vanarticlegoodrevid': self.params.goodid, 'type': self.params.type, 'count': self.params.count }; switch( Twinkle.getPref('userTalkPageMode') ) { case 'tab': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' ); break; case 'blank': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; case 'window': /* falls through */ default: window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; } } query = { 'action': 'edit', 'title': self.params.pagename, 'summary': summary, 'token': edittoken, 'undo': lastrevid, 'undoafter': self.params.goodid, 'basetimestamp': touched, 'starttimestamp': starttimestamp, 'watchlist' : Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined, 'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined }; Morebits.wiki.actionCompleted.redirect = self.params.pagename; Morebits.wiki.actionCompleted.notice = "Reversion completed"; var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, Twinkle.fluff.callbacks.complete, self.statelem); wikipedia_api.params = self.params; wikipedia_api.post(); }, complete: function (self) { self.statelem.info("done"); } }; Twinkle.fluff.formatSummaryPostfix = function(stringToAdd) { if (stringToAdd) { stringToAdd = ': ' + Morebits.string.toUpperCaseFirstChar(stringToAdd); if (stringToAdd.search(/[.?!;]$/) === -1) { stringToAdd = stringToAdd + '.'; } return stringToAdd; } else { return '.'; } }; Twinkle.fluff.init = function twinklefluffinit() { if (twinkleUserAuthorized) { // a list of usernames, usually only bots, that vandalism revert is jumped over, that is // if vandalism revert was chosen on such username, then it's target is on the revision before. // This is for handeling quick bots that makes edits seconds after the original edit is made. // This only affect vandalism rollback, for good faith rollback, it will stop, indicating a bot // has no faith, and for normal rollback, it will rollback that edit. Twinkle.fluff.whiteList = [ 'AnomieBOT', 'ClueBot NG', 'SineBot' ]; if ( Morebits.queryString.exists( 'twinklerevert' ) ) { Twinkle.fluff.auto(); } else { Twinkle.fluff.normal(); } } }; /* **************************************** *** twinklespeedy.js: CSD module **************************************** * Mode of invocation: Tab ("CSD") * Active on: Non-special, existing pages * Config directives in: TwinkleConfig * * NOTE FOR DEVELOPERS: * If adding a new criterion, check out the default values of the CSD preferences * in twinkle.header.js, and add your new criterion to those if you think it would * be good. */ Twinkle.speedy = function twinklespeedy() { // Disable on: // * special pages // * non-existent pages if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) { return; } twAddPortletLink( Twinkle.speedy.callback, "Del", "tw-csd", Morebits.userIsInGroup('sysop') ? "Delete page" : "Request deletion" ); }; // This function is run when the CSD tab/header link is clicked Twinkle.speedy.callback = function twinklespeedyCallback() { if ( !twinkleUserAuthorized ) { alert("Your account is too new to use Twinkle."); return; } Twinkle.speedy.initDialog(Morebits.userIsInGroup( 'sysop' ) ? Twinkle.speedy.callback.evaluateSysop : Twinkle.speedy.callback.evaluateUser, true); }; Twinkle.speedy.dialog = null; // used by unlink feature // Prepares the speedy deletion dialog and displays it Twinkle.speedy.initDialog = function twinklespeedyInitDialog(callbackfunc) { var dialog; Twinkle.speedy.dialog = new Morebits.simpleWindow( Twinkle.getPref('speedyWindowWidth'), Twinkle.getPref('speedyWindowHeight') ); dialog = Twinkle.speedy.dialog; dialog.setTitle( "Choose criteria for deletion" ); dialog.setScriptName( "Twinkle" ); //dialog.addFooterLink( "Quick deletion policy", "Wikipedia:Deletion policy#Quick deletion" ); dialog.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#speedy" ); var form = new Morebits.quickForm( callbackfunc, (Twinkle.getPref('speedySelectionStyle') === 'radioClick' ? 'change' : null) ); if( Morebits.userIsInGroup( 'sysop' ) ) { form.append( { type: 'checkbox', list: [ { label: 'Tag page only, don\'t delete', value: 'tag_only', name: 'tag_only', tooltip: 'If you just want to tag the page, instead of deleting it now', checked : Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { var cForm = event.target.form; var cChecked = event.target.checked; // enable/disable talk page checkbox if (cForm.talkpage) { cForm.talkpage.disabled = cChecked; cForm.talkpage.checked = !cChecked && Twinkle.getPref('deleteTalkPageOnDelete'); } // enable/disable redirects checkbox cForm.redirects.disabled = cChecked; cForm.redirects.checked = !cChecked; // enable/disable notify checkbox cForm.notify.disabled = !cChecked; cForm.notify.checked = cChecked; // enable/disable multiple cForm.multiple.disabled = !cChecked; cForm.multiple.checked = false; Twinkle.speedy.callback.dbMultipleChanged(cForm, false); event.stopPropagation(); } } ] } ); form.append( { type: 'header', label: 'Delete-related options' } ); if (mw.config.get('wgNamespaceNumber') % 2 === 0 && (mw.config.get('wgNamespaceNumber') !== 2 || (/\//).test(mw.config.get('wgTitle')))) { // hide option for user pages, to avoid accidentally deleting user talk page form.append( { type: 'checkbox', list: [ { label: 'Also delete talk page', value: 'talkpage', name: 'talkpage', tooltip: "This option deletes the page's talk page in addition. If you choose the F8 (moved to Commons) criterion, this option is ignored and the talk page is *not* deleted.", checked: Twinkle.getPref('deleteTalkPageOnDelete'), disabled: Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { event.stopPropagation(); } } ] } ); } form.append( { type: 'checkbox', list: [ { label: 'Also delete all redirects', value: 'redirects', name: 'redirects', tooltip: "This option deletes all incoming redirects in addition. Avoid this option for procedural (e.g. move/merge) deletions.", checked: true, disabled: Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { event.stopPropagation(); } } ] } ); form.append( { type: 'header', label: 'Tag-related options' } ); } form.append( { type: 'checkbox', list: [ { label: 'Notify page creator if possible', value: 'notify', name: 'notify', tooltip: "A notification template will be placed on the talk page of the creator, IF you have a notification enabled in your Twinkle preferences " + "for the criterion you choose AND this box is checked. The creator may be welcomed as well.", checked: !Morebits.userIsInGroup( 'sysop' ) || Twinkle.getPref('deleteSysopDefaultToTag'), disabled: Morebits.userIsInGroup( 'sysop' ) && !Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { event.stopPropagation(); } } ] } ); form.append( { type: 'div', name: 'work_area', label: 'Failed to initialize the CSD module. Please try again, or tell the Twinkle developers about the issue.' } ); if( Twinkle.getPref( 'speedySelectionStyle' ) !== 'radioClick' ) { form.append( { type: 'submit' } ); } var result = form.render(); dialog.setContent( result ); dialog.display(); Twinkle.speedy.callback.dbMultipleChanged( result, false ); }; Twinkle.speedy.callback.dbMultipleChanged = function twinklespeedyCallbackDbMultipleChanged(form, checked) { var namespace = mw.config.get('wgNamespaceNumber'); var value = checked; var work_area = new Morebits.quickForm.element( { type: 'div', name: 'work_area' } ); if (checked && Twinkle.getPref('speedySelectionStyle') === 'radioClick') { work_area.append( { type: 'div', label: 'When finished choosing criteria, click:' } ); work_area.append( { type: 'button', name: 'submit-multiple', label: 'Submit Query', event: function( event ) { Twinkle.speedy.callback.evaluateUser( event ); event.stopPropagation(); } } ); } var radioOrCheckbox = (value ? 'checkbox' : 'radio'); /* if (namespace % 2 === 1 && namespace !== 3) { // talk pages, but not user talk pages work_area.append( { type: 'header', label: 'Talk pages' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.talkList } ); } switch (namespace) { case 0: // article case 1: // talk work_area.append( { type: 'header', label: 'Articles' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getArticleList(value) } ); break; case 2: // user case 3: // user talk work_area.append( { type: 'header', label: 'User pages' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.userList } ); break; case 6: // file case 7: // file talk work_area.append( { type: 'header', label: 'Files' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getFileList(value) } ); break; case 10: // template case 11: // template talk work_area.append( { type: 'header', label: 'Templates' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.templateList } ); break; case 14: // category case 15: // category talk work_area.append( { type: 'header', label: 'Categories' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.categoryList } ); break; default: break; } */ work_area.append( { type: 'header', label: 'General criteria' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getGeneralList(value) }); /* work_area.append( { type: 'header', label: 'Redirects' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.redirectList } ); */ var old_area = Morebits.quickForm.getElements(form, "work_area")[0]; form.replaceChild(work_area.render(), old_area); }; Twinkle.speedy.talkList = [ /*{ label: 'G8: Talk pages with no page belonging to it', value: 'talk', tooltip: 'This does not include any page that is useful to the project - for example user talk pages, talk page archives, and talk pages for files that exist on Wikimedia Commons.' }*/ ]; // this is a function to allow for db-multiple filtering Twinkle.speedy.getFileList = function twinklespeedyGetFileList(multiple) { var result = []; /*result.push({ label: 'F1: Not allowed', value: 'prohibitedimage', tooltip: 'Most media uploads are not allowed on Simple English Wikipedia. They should be uploaded to Wikimedia Commons instead. There are a few exceptions to this rule. Firstly, all spoken articles should be uploaded here, as they are for local use. Secondly, there are some logos that Commons does not accept, but are needed here, for example Image:Wiki.png, which is used as the Wikipedia logo.' });*/ return result; }; Twinkle.speedy.getArticleList = function twinklespeedyGetArticleList(multiple) { var result = []; /*result.push({ label: 'A1: Little or no meaning', value: 'nocontext', tooltip: 'Is very short and providing little or no meaning (e.g., "He is a funny man that has created Factory and the Hacienda. And, by the way, his wife is great."). Having a small amount of content is not a reason to delete if it has useful information.' }); result.push({ label: 'A2: No content', value: 'nocontent', tooltip: 'Has no content. This includes any article consisting only of links (including hyperlinks, category tags and "see also" sections), a rephrasing of the title, and/or attempts to correspond with the person or group named by its title. This does not include disambiguation pages.' }); result.push({ label: 'A3: Article that exists on another Wikimedia project', value: 'transwiki', tooltip: 'Has been copied and pasted from another Wikipedia: Any article or section from an article that has been copied and pasted with little or no change.' }); result.push({ label: 'A4: People, groups, companies, products, services or websites that do not claim to be notable.', value: 'notability', tooltip: 'An article about a real person, group of people, band, club, company, product, service or or web content that does not say why it is important. If not everyone agrees that the subject is not notable or there has been a previous RfD, the article may not be quickly deleted, and should be discussed at RfD instead.' }); result.push({ label: 'A5: Not written in English', value: 'foreign', tooltip: 'Any article that is not written in English. An article that is written in any other languages but English.' }); result.push({ label: 'A6: Obvious hoax', value: 'hoax', tooltip: 'Is an obvious hoax. An article that is surely fake or impossible.' });*/ return result; }; Twinkle.speedy.categoryList = [ /*{ label: 'C1: Empty categories', value: 'catempty', tooltip: '(with no articles or subcategories for at least four days) whose only content includes links to parent categories. However, this can not be used on categories still being discussed on WP:RfD, or disambiguation categories. If the category wasn\'t newly made, it is possible that it used to have articles, and more inspection is needed.' }, { label: 'C2: Quick renaming', value: 'catqr', tooltip: 'Empty categories that have already been renamed.' }, { label: 'C3: Template categories', value: 'catfd', tooltip: 'If a category contains articles from only one template (such as Category:Cleanup needed from \{\{cleanup\}\}) and the template is deleted after being discussed, the category can also be deleted without being discussed.' }*/ ]; Twinkle.speedy.userList = [ /*{ label: 'U1: User request', value: 'userreq', tooltip: 'User pages can be deleted if its user wants to, but there are some exceptions.' }, { label: 'U2: Nonexistent user', value: 'nouser', tooltip: 'User pages of users that do not exist. Administrators should check Special:Contributions and Special:DeletedContributions.' }*/ ]; Twinkle.speedy.templateList = [ /*{ label: 'T2: They are deprecated or replaced by a newer template and are completely unused and not linked to.', value: 'replaced', tooltip: 'For any template that should not be deleted quickly, use Wikipedia:Requests for deletion.' }*/ //}); // return result; ]; Twinkle.speedy.getGeneralList = function twinklespeedyGetGeneralList(multiple) { var result = []; if (!multiple) { result.push({ label: 'Custom rationale' + (Morebits.userIsInGroup('sysop') ? ' (custom deletion reason)' : ' using {'+'{delete|reason}}'), value: 'reason', tooltip: 'You can enter an custom reason.' }); } /*result.push({ label: 'G1: Nonsense', value: 'nonsense', tooltip: 'All of the text is nonsense. Nonsense includes content that does not make sense or is not meaningful. However, this does not include bad writing, bad words, vandalism, things that are fake or impossible, or parts which are not in English. ' }); result.push({ label: 'G2: Test page', value: 'test', tooltip: 'It is a test page, such as "Can I really create a page here?".' }); result.push({ label: 'G3: Complete vandalism', value: 'vandalism', tooltip: 'The content is completely vandalism.' }); result.push({ label: 'G4: Recreation of deleted material already deleted at RfD', value: 'repost', tooltip: 'Creation of content that is already deleted. It includes an identical or similar copy, with any title, of a page that was deleted, after being discussed in Requests for deletion, unless it was undeleted due to another discussion or was recreated in the user space. Before deleting again, the Administrator should be sure that the content is similar and not just a new article on the same subject. This rule cannot be used if the content had already been quickly deleted before.' }); if (!multiple) { result.push({ label: 'G6: History merge', value: 'histmerge', tooltip: 'Temporarily deleting a page in order to merge page histories' }); result.push({ label: 'G6: Move', value: 'move', tooltip: 'Making way for a noncontroversial move like reversing a redirect' }); result.push({ label: 'G6: RfD', value: 'afd', tooltip: 'An admin has closed a RfD as "delete".' }); } result.push({ label: 'G6: Housekeeping', value: 'g6', tooltip: 'Other non-controversial "housekeeping" tasks' }); result.push({ label: 'G7: Author requests deletion, or author blanked', value: 'author', tooltip: 'Any page whose original author wants deletion, can be quickly deleted, but only if most of the page was written by that author and was created as a mistake. If the author blanks the page, this can mean that he or she wants it deleted.' }); result.push({ label: 'G8: Pages dependent on a non-existent or deleted page', value: 'talk', tooltip: '... can be deleted, unless they contain discussion on deletion that can\'t be found anywhere else. Subpages of a talk page can only be deleted under this rule if their top-level page does not exist. This also applies to broken redirects. However, this cannot be used on user talk pages or talk pages of images on Commons.' }); if (!multiple) { result.push({ label: 'G8: Subpages with no parent page', value: 'subpage', tooltip: 'This excludes any page that is useful to the project, and in particular: deletion discussions that are not logged elsewhere, user and user talk pages, talk page archives, plausible redirects that can be changed to valid targets, and file pages or talk pages for files that exist on Wikimedia Commons.' }); } result.push({ label: 'G10: Attack page', value: 'attack', tooltip: 'Pages that were only created to insult a person or thing (such as "John Q. Doe is dumb"). This includes articles on a living person that is insult and without sources, where there is no NPOV version in the edit history to revert to.' }); result.push({ label: 'G11: Obvious advertising', value: 'spam', tooltip: 'Pages which were created only to say good things about a company, item, group or service and which would need to be written again so that they can sound like an encyclopedia. However, simply having a company, item, group or service as its subject does not mean that an article can be deleted because of this rule: an article that is obvious advertising should have content that shouldn\'t be in an encyclopedia. If a page has already gone through RfD or QD and was not deleted, it should not be quickly deleted using this rule.' }); result.push({ label: 'G12: Obviously breaking copyright law', value: 'copyvio', tooltip: 'Obviously breaking copyright law like a page which is 1) Copied from another website which does not have a license that can be used with Wikipedia; 2) Containing no content in the page history that is worth being saved. 3) Made by one person instead of being created on wiki and then copied by another website such as one of the many Wikipedia mirror websites. 4) Added by someone who doesn\'t tell if he got permission to do so or not, or if his claim has a large chance of not being true;' });*/ return result; }; Twinkle.speedy.redirectList = [ /*{ label: 'R1: Redirects to a non-existent page.', value: 'redirnone', tooltip: 'Redirects to a non-existent page.' }, { label: 'R2: Redirects from mainspace to any other namespace except the Category:, Template:, Wikipedia:, Help: and Portal: namespaces', value: 'rediruser', tooltip: '(this does not include the Wikipedia shortcut pseudo-namespaces). If this was the result of a page move, consider waiting a day or two before deleting the redirect' }, { label: 'R3: Redirects as a result of an implausible typo that were recently created', value: 'redirtypo', tooltip: 'However, redirects from common misspellings or misnomers are generally useful, as are redirects in other languages' }*/ ]; Twinkle.speedy.normalizeHash = { 'reason': 'db', 'nonsense': 'g1', 'test': 'g2', 'vandalism': 'g3', 'hoax': 'g3', 'repost': 'g4', 'histmerge': 'g6', 'move': 'g6', 'afd': 'g6', 'g6': 'g6', 'author': 'g7', 'talk': 'g8', 'subpage': 'g8', 'attack': 'g10', 'spam': 'g11', 'copyvio': 'g12', 'nocontext': 'a1', 'nocontent': 'a2', 'transwiki': 'a3', 'notability': 'a4', 'foreign': 'a5', 'hoax': 'a6', 'redirnone': 'r1', 'rediruser': 'r2', 'redirtypo': 'r3', 'prohibitedimage': 'f1', 'catempty': 'c1', 'catqr': 'c2', 'catfd': 'c3', 'userreq': 'u1', 'nouser': 'u2', 'replaced':'t2' }; // keep this synched with [[MediaWiki:Deletereason-dropdown]] Twinkle.speedy.reasonHash = { 'reason': '', 'nonsense': 'was all nonsense', 'test': 'was a test page', 'vandalism': 'was vandalism', 'pagemove': 'was a redirect created during cleanup of page move vandalism', 'repost': 'was a copy of a page that was deleted by RfD', 'histmerge': 'was in the way of trying to fix or clean up something', 'move': 'was in the way of making a move', 'afd': 'was closed as delete in a RfD', 'g6': 'was housekeeping', 'author': 'was asked to be deleted by the author', 'blanked': 'was implied to be deleted by the author', 'talk': 'was a talk page of a page that does not exist', 'attack': 'was an attack page', 'spam': 'was advertising', 'copyvio': 'was breaking copyright law', 'nocontext': 'was a page that had little or no meaning', 'nocontent': 'was a page that had no content', 'transwiki': 'was copied from another Wikipedia', 'notability': 'was a page that didn\'t say why the subject was notable', 'foreign': 'was not written in English', 'hoax': 'was obviously a hoax (not true)', 'redirnone': 'was a redirect to a page that does not exist', 'rediruser': 'was a redirect to the Talk:, User: or User talk: space', 'redirtypo': 'was a redirect with an uncommon typo', 'prohibitedimage': 'was an image/media that is not allowed on Wikipedia', 'catempty': 'was an empty category', 'catqr': 'was a renamed category', 'catfd': 'was a category containing articles from a now deleted template', 'userreq': 'was a user page whose user requested deletion', 'nouser': 'was a user page of a user that did not exist', 'replaced': 'was deprecated or replaced by a newer template and are completely unused and not linked to' }; Twinkle.speedy.callbacks = { sysop: { main: function( params ) { var thispage = new Morebits.wiki.page( mw.config.get('wgPageName'), "Deleting page" ); // delete page var reason; if (params.normalized === 'db') { reason = prompt("Enter the deletion summary to use, which will be entered into the deletion log:", ""); } else { var presetReason = "[[WP:QD#" + params.normalized.toUpperCase() + "|" + params.normalized.toUpperCase() + "]]: " + params.reason; if (Twinkle.getPref("promptForSpeedyDeletionSummary").indexOf(params.normalized) !== -1) { reason = prompt("Enter the deletion summary to use, or press OK to accept the automatically generated one.", presetReason); } else { reason = presetReason; } } if (!reason || !reason.replace(/^\s*/, "").replace(/\s*$/, "")) { Morebits.status.error("Asking for reason", "you didn't give one. I don't know... what with admins and their apathetic antics... I give up..."); return; } thispage.setEditSummary( reason + Twinkle.getPref('deletionSummaryAd') ); thispage.deletePage(); // delete talk page if (params.deleteTalkPage && params.normalized !== 'f8' && document.getElementById( 'ca-talk' ).className !== 'new') { var talkpage = new Morebits.wiki.page( Morebits.wikipedia.namespaces[ mw.config.get('wgNamespaceNumber') + 1 ] + ':' + mw.config.get('wgTitle'), "Deleting talk page" ); talkpage.setEditSummary('Talk page of deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd')); talkpage.deletePage(); } // promote Unlink tool var $link, $bigtext; if( mw.config.get('wgNamespaceNumber') === 6 && params.normalized !== 'f8' ) { $link = $('<a/>', { 'href': '#', 'text': 'click here to go to the Unlink tool', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' }, 'click': function(){ Morebits.wiki.actionCompleted.redirect = null; Twinkle.speedy.dialog.close(); Twinkle.unlink.callback("Removing usages of and/or links to deleted file " + mw.config.get('wgPageName')); } }); $bigtext = $('<span/>', { 'text': 'To orphan backlinks and remove instances of file usage', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' } }); Morebits.status.info($bigtext[0], $link[0]); } else if (params.normalized !== 'f8') { $link = $('<a/>', { 'href': '#', 'text': 'click here to go to the Unlink tool', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' }, 'click': function(){ Morebits.wiki.actionCompleted.redirect = null; Twinkle.speedy.dialog.close(); Twinkle.unlink.callback("Removing links to deleted page " + mw.config.get('wgPageName')); } }); $bigtext = $('<span/>', { 'text': 'To orphan backlinks', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' } }); Morebits.status.info($bigtext[0], $link[0]); } // open talk page of first contributor if( params.openusertalk ) { thispage = new Morebits.wiki.page( mw.config.get('wgPageName') ); // a necessary evil, in order to clear incorrect status text thispage.setCallbackParameters( params ); thispage.lookupCreator( Twinkle.speedy.callbacks.sysop.openUserTalkPage ); } // delete redirects if (params.deleteRedirects) { var query = { 'action': 'query', 'list': 'backlinks', 'blfilterredir': 'redirects', 'bltitle': mw.config.get('wgPageName'), 'bllimit': 5000 // 500 is max for normal users, 5000 for bots and sysops }; var wikipedia_api = new Morebits.wiki.api( 'getting list of redirects...', query, Twinkle.speedy.callbacks.sysop.deleteRedirectsMain, new Morebits.status( 'Deleting redirects' ) ); wikipedia_api.params = params; wikipedia_api.post(); } }, openUserTalkPage: function( pageobj ) { pageobj.getStatusElement().unlink(); // don't need it anymore var user = pageobj.getCreator(); var statusIndicator = new Morebits.status('Opening user talk page edit form for ' + user, 'opening...'); var query = { 'title': 'User talk:' + user, 'action': 'edit', 'preview': 'yes', 'vanarticle': mw.config.get('wgPageName').replace(/_/g, ' ') }; switch( Twinkle.getPref('userTalkPageMode') ) { case 'tab': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' ); break; case 'blank': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; case 'window': /* falls through */ default : window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; } statusIndicator.info( 'complete' ); }, deleteRedirectsMain: function( apiobj ) { var xmlDoc = apiobj.getXML(); var $snapshot = $(xmlDoc).find('backlinks bl'); var total = $snapshot.length; if( !total ) { return; } var statusIndicator = apiobj.statelem; statusIndicator.status("0%"); var onsuccess = function( apiobj ) { var obj = apiobj.params.obj; var total = apiobj.params.total; var now = parseInt( 100 * ++(apiobj.params.current)/total, 10 ) + '%'; obj.update( now ); apiobj.statelem.unlink(); if( apiobj.params.current >= total ) { obj.info( now + ' (completed)' ); Morebits.wiki.removeCheckpoint(); } }; Morebits.wiki.addCheckpoint(); var params = $.extend( {}, apiobj.params ); params.current = 0; params.total = total; params.obj = statusIndicator; $snapshot.each(function(key, value) { var title = $(value).attr('title'); var page = new Morebits.wiki.page(title, 'Deleting redirect "' + title + '"'); page.setEditSummary('Redirect to deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd')); page.deletePage(onsuccess); }); } }, user: { main: function(pageobj) { var statelem = pageobj.getStatusElement(); if (!pageobj.exists()) { statelem.error( "It seems that the page doesn't exist; perhaps it has already been deleted" ); return; } var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); statelem.status( 'Checking for tags on the page...' ); // check for existing deletion tags var tag = /(?:\{\{\s*(qd|qd-multiple|db|delete|db-.*?)(?:\s*\||\s*\}\}))/.exec( text ); if( tag ) { statelem.error( [ Morebits.htmlNode( 'strong', tag[1] ) , " is already placed on the page." ] ); return; } var xfd = /(?:\{\{([rsaiftcm]fd|md1|proposed deletion)[^{}]*?\}\})/i.exec( text ); if( xfd && !confirm( "The deletion-related template {{" + xfd[1] + "}} was found on the page. Do you still want to add a CSD template?" ) ) { return; } var code, parameters, i; if (params.normalizeds.length > 1) { code = "{{QD-multiple"; var breakFlag = false; $.each(params.normalizeds, function(index, norm) { code += "|" + norm.toUpperCase(); parameters = Twinkle.speedy.getParameters(params.values[index], norm, statelem); if (!parameters) { breakFlag = true; return false; // the user aborted } for (i in parameters) { if (typeof parameters[i] === 'string' && !parseInt(i, 10)) { // skip numeric parameters - {{db-multiple}} doesn't understand them code += "|" + i + "=" + parameters[i]; } } }); if (breakFlag) { return; } code += "}}"; params.utparams = []; } else { parameters = Twinkle.speedy.getParameters(params.values[0], params.normalizeds[0], statelem); if (!parameters) { return; // the user aborted } code = "{{delete|" + params.normalizeds; for (i in parameters) { if (typeof parameters[i] === 'string') { code += "|" + i + "=" + parameters[i]; } } code += "|editor=" + mw.config.get("wgUserName") + "|date=~~~~~"; code += "}}"; params.utparams = Twinkle.speedy.getUserTalkParameters(params.normalizeds[0], parameters); } var thispage = new Morebits.wiki.page(mw.config.get('wgPageName')); // patrol the page, if reached from Special:NewPages if( Twinkle.getPref('markSpeedyPagesAsPatrolled') ) { thispage.patrol(); } // Wrap SD template in noinclude tags if we are in template space. // Won't work with userboxes in userspace, or any other transcluded page outside template space if (mw.config.get('wgNamespaceNumber') === 10) { // Template: code = "<noinclude>" + code + "</noinclude>"; } // Remove tags that become superfluous with this action if (mw.config.get('wgNamespaceNumber') === 6) { // remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons text = text.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)[^}]*\}\}/gi, ""); } // Generate edit summary for edit var editsummary; if (params.normalizeds.length > 1) { editsummary = 'Requesting quick deletion ('; $.each(params.normalizeds, function(index, norm) { editsummary += '[[WP:QD#' + norm.toUpperCase() + '|QD ' + norm.toUpperCase() + ']], '; }); editsummary = editsummary.substr(0, editsummary.length - 2); // remove trailing comma editsummary += ').'; } else if (params.normalizeds[0] === "db") { editsummary = 'Requesting deletion with criteria \"' + parameters["1"] + '\".'; } else if (params.values[0] === "histmerge") { editsummary = "Requesting history merge with [[" + parameters["1"] + "]]"; } else { editsummary = "Requesting deletion (" + params.normalizeds[0].toUpperCase() + ")"; } pageobj.setPageText(code + ((params.normalizeds.indexOf('g10') !== -1) ? '' : ("\n" + text) )); // cause attack pages to be blanked pageobj.setEditSummary(editsummary + Twinkle.getPref('summaryAd')); pageobj.setWatchlist(params.watch); pageobj.setCreateOption('nocreate'); pageobj.save(Twinkle.speedy.callbacks.user.tagComplete); }, tagComplete: function(pageobj) { var params = pageobj.getCallbackParameters(); // Notification to first contributor if (params.usertalk) { var callback = function(pageobj) { var initialContrib = pageobj.getCreator(); // don't notify users when their user talk page is nominated if (initialContrib === mw.config.get('wgTitle') && mw.config.get('wgNamespaceNumber') === 3) { Morebits.status.warn("Notifying initial contributor: this user created their own user talk page; skipping notification"); return; } var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")"), notifytext, i; // specialcase "db" and "db-multiple" if (params.normalizeds.length > 1) { notifytext = "\n{{subst:QD-notice-multiple|page=" + mw.config.get('wgPageName'); var count = 2; $.each(params.normalizeds, function(index, norm) { notifytext += "|" + (count++) + "=" + norm.toUpperCase(); }); } else if (params.normalizeds[0] === "db") { notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds; } else { notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds; } for (i in params.utparams) { if (typeof params.utparams[i] === 'string') { notifytext += "|" + i + "=" + params.utparams[i]; } } notifytext += (params.welcomeuser ? "" : "|nowelcome=yes") + "}} ~~~~"; usertalkpage.setAppendText(notifytext); usertalkpage.setEditSummary("Notification: quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); usertalkpage.setCreateOption('recreate'); usertalkpage.setFollowRedirect(true); usertalkpage.append(); // add this nomination to the user's userspace log, if the user has enabled it if (params.lognomination) { Twinkle.speedy.callbacks.user.addToLog(params, initialContrib); } }; var thispage = new Morebits.wiki.page(mw.config.get('wgPageName')); thispage.lookupCreator(callback); } // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name else if (params.lognomination) { Twinkle.speedy.callbacks.user.addToLog(params, null); } }, // note: this code is also invoked from twinkleimage // the params used are: // for CSD: params.values, params.normalizeds (note: normalizeds is an array) // for DI: params.fromDI = true, params.type, params.normalized (note: normalized is a string) addToLog: function(params, initialContrib) { var wikipedia_page = new Morebits.wiki.page("User:" + mw.config.get('wgUserName') + "/" + Twinkle.getPref('speedyLogPageName'), "Adding entry to userspace log"); params.logInitialContrib = initialContrib; wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.speedy.callbacks.user.saveLog); }, saveLog: function(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); // add blurb if log page doesn't exist if (!pageobj.exists()) { text = "This is a log of all deletion nominations made by this user using [[mh:dev:Twinkle|Twinkle]]'s QD module.\n\n" + "If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and " + "request deletion for this page.\n"; if (Morebits.userIsInGroup("sysop")) { text += "\nThis log does not track outright speedy deletions made using Twinkle.\n"; } } // create monthly header var date = new Date(); var headerRe = new RegExp("^==+\\s*" + date.getUTCMonthName() + "\\s+" + date.getUTCFullYear() + "\\s*==+", "m"); if (!headerRe.exec(text)) { text += "\n\n=== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==="; } text += "\n# [[:" + mw.config.get('wgPageName') + "]]: "; if (params.fromDI) { text += "DI [[WP:QD#" + params.normalized.toUpperCase() + "|QD " + params.normalized.toUpperCase() + "]] (" + params.type + ")"; } else { if (params.normalizeds.length > 1) { text += "multiple criteria ("; $.each(params.normalizeds, function(index, norm) { text += "[[WP:QD#" + norm.toUpperCase() + "|" + norm.toUpperCase() + ']], '; }); text = text.substr(0, text.length - 2); // remove trailing comma text += ')'; } else if (params.normalizeds[0] === "db") { text += "{{tl|QD}}"; } else { text += "[[WP:QD#" + params.normalizeds[0].toUpperCase() + "|CSD " + params.normalizeds[0].toUpperCase() + "]] ({{tl|db-" + params.values[0] + "}})"; } } if (params.logInitialContrib) { text += "; notified {{user|" + params.logInitialContrib + "}}"; } text += " ~~~~~\n"; pageobj.setPageText(text); pageobj.setEditSummary("Logging quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); pageobj.setCreateOption("recreate"); pageobj.save(); } } }; // prompts user for parameters to be passed into the speedy deletion tag Twinkle.speedy.getParameters = function twinklespeedyGetParameters(value, normalized, statelem) { var parameters = []; switch( normalized ) { case 'db': var dbrationale = prompt('Please enter a custom reason. \n\"This page can be quickly deleted because:\"', ""); if (!dbrationale || !dbrationale.replace(/^\s*/, "").replace(/\s*$/, "")) { statelem.error( 'You must specify a reason. Aborted by user.' ); return null; } parameters["1"] = dbrationale; break; case 'g12': var url = prompt( '[QD G12] Please enter the URL if available, including the "http://":', "" ); if (url === null) { statelem.error( 'Aborted by user.' ); return null; } parameters.url = url; break; default: var defaultreason = prompt('You can enter more details here. \n' + "Just click OK if you don't want or need to.", ""); if (defaultreason === null) { return true; // continue to next tag } else if (defaultreason !== "") { parameters["2"] = defaultreason; } break; } return parameters; }; // function for processing talk page notification template parameters Twinkle.speedy.getUserTalkParameters = function twinklespeedyGetUserTalkParameters(normalized, parameters) { var utparams = []; switch (normalized) { case 'db': utparams["2"] = parameters["1"]; break; case 'a10': utparams.key1 = "article"; utparams.value1 = parameters.article; break; default: break; } return utparams; }; Twinkle.speedy.resolveCsdValues = function twinklespeedyResolveCsdValues(e) { var values = (e.target.form ? e.target.form : e.target).getChecked('csd'); if (values.length === 0) { alert( "Please select a criterion!" ); return null; } return values; }; Twinkle.speedy.callback.evaluateSysop = function twinklespeedyCallbackEvaluateSysop(e) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var form = (e.target.form ? e.target.form : e.target); var tag_only = form.tag_only; if( tag_only && tag_only.checked ) { Twinkle.speedy.callback.evaluateUser(e); return; } var value = Twinkle.speedy.resolveCsdValues(e)[0]; if (!value) { return; } var normalized = Twinkle.speedy.normalizeHash[ value ]; var params = { value: value, normalized: normalized, watch: Twinkle.getPref('watchSpeedyPages').indexOf( normalized ) !== -1, reason: Twinkle.speedy.reasonHash[ value ], openusertalk: Twinkle.getPref('openUserTalkPageOnSpeedyDelete').indexOf( normalized ) !== -1, deleteTalkPage: form.talkpage && form.talkpage.checked, deleteRedirects: form.redirects.checked }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Twinkle.speedy.callbacks.sysop.main( params ); }; Twinkle.speedy.callback.evaluateUser = function twinklespeedyCallbackEvaluateUser(e) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var form = (e.target.form ? e.target.form : e.target); if (e.target.type === "checkbox") { return; } var values = Twinkle.speedy.resolveCsdValues(e); if (!values) { return; } //var multiple = form.multiple.checked; var normalizeds = []; $.each(values, function(index, value) { var norm = Twinkle.speedy.normalizeHash[ value ]; // for sysops only if (['f4', 'f5', 'f6', 'f11'].indexOf(norm) !== -1) { alert("Tagging with F4, F5, F6, and F11 is not possible using the CSD module. Try using DI instead, or unchecking \"Tag page only\" if you meant to delete the page."); return; } normalizeds.push(norm); }); // analyse each criterion to determine whether to watch the page/notify the creator var watchPage = false; $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('watchSpeedyPages').indexOf(norm) !== -1) { watchPage = true; return false; // break } }); var notifyuser = false; if (form.notify.checked) { $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('notifyUserOnSpeedyDeletionNomination').indexOf(norm) !== -1) { notifyuser = true; return false; // break } }); } var welcomeuser = false; if (notifyuser) { $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('welcomeUserOnSpeedyDeletionNotification').indexOf(norm) !== -1) { welcomeuser = true; return false; // break } }); } var csdlog = false; if (Twinkle.getPref('logSpeedyNominations')) { $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('noLogOnSpeedyNomination').indexOf(norm) === -1) { csdlog = true; return false; // break } }); } var params = { values: values, normalizeds: normalizeds, watch: watchPage, usertalk: notifyuser, welcomeuser: welcomeuser, lognomination: csdlog }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging page"); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.speedy.callbacks.user.main); }; /* **************************************** *** twinkleunlink.js: Unlink module **************************************** * Mode of invocation: Tab ("Unlink") * Active on: Non-special pages * Config directives in: TwinkleConfig */ Twinkle.unlink = function twinkleunlink() { if( mw.config.get('wgNamespaceNumber') < 0 ) { return; } twAddPortletLink( Twinkle.unlink.callback, "Unlink", "tw-unlink", "Unlink backlinks" ); }; Twinkle.unlink.getChecked2 = function twinkleunlinkGetChecked2( nodelist ) { if( !( nodelist instanceof NodeList ) && !( nodelist instanceof HTMLCollection ) ) { return nodelist.checked ? [ nodelist.values ] : []; } var result = []; for(var i = 0; i < nodelist.length; ++i ) { if( nodelist[i].checked ) { result.push( nodelist[i].values ); } } return result; }; // the parameter is used when invoking unlink from admin speedy Twinkle.unlink.callback = function(presetReason) { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setTitle( "Unlink backlinks" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#unlink" ); var form = new Morebits.quickForm( Twinkle.unlink.callback.evaluate ); form.append( { type: 'textarea', name: 'reason', label: 'Reason: ', value: (presetReason ? presetReason : '') } ); var query; if(mw.config.get('wgNamespaceNumber') === 6) { // File: query = { 'action': 'query', 'list': [ 'backlinks', 'imageusage' ], 'bltitle': mw.config.get('wgPageName'), 'iutitle': mw.config.get('wgPageName'), 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops 'iulimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops 'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages. }; } else { query = { 'action': 'query', 'list': 'backlinks', 'bltitle': mw.config.get('wgPageName'), 'blfilterredir': 'nonredirects', 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops 'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages. }; } var wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.unlink.callbacks.display.backlinks ); wikipedia_api.params = { form: form, Window: Window, image: mw.config.get('wgNamespaceNumber') === 6 }; wikipedia_api.post(); var root = document.createElement( 'div' ); root.style.padding = '15px'; // just so it doesn't look broken Morebits.status.init( root ); wikipedia_api.statelem.status( "loading..." ); Window.setContent( root ); Window.display(); }; Twinkle.unlink.callback.evaluate = function twinkleunlinkCallbackEvaluate(event) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! Twinkle.unlink.backlinksdone = 0; Twinkle.unlink.imageusagedone = 0; function processunlink(pages, imageusage) { var statusIndicator = new Morebits.status((imageusage ? 'Unlinking instances of file usage' : 'Unlinking backlinks'), '0%'); var total = pages.length; // removing doubling of this number - no apparent reason for it Morebits.wiki.addCheckpoint(); if( !pages.length ) { statusIndicator.info( '100% (completed)' ); Morebits.wiki.removeCheckpoint(); return; } // get an edit token var params = { reason: reason, imageusage: imageusage, globalstatus: statusIndicator, current: 0, total: total }; for (var i = 0; i < pages.length; ++i) { var myparams = $.extend({}, params); var articlepage = new Morebits.wiki.page(pages[i], 'Unlinking in article "' + pages[i] + '"'); articlepage.setCallbackParameters(myparams); articlepage.load(imageusage ? Twinkle.unlink.callbacks.unlinkImageInstances : Twinkle.unlink.callbacks.unlinkBacklinks); } } var reason = event.target.reason.value; var backlinks, imageusage; if( event.target.backlinks ) { backlinks = Twinkle.unlink.getChecked2(event.target.backlinks); } if( event.target.imageusage ) { imageusage = Twinkle.unlink.getChecked2(event.target.imageusage); } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( event.target ); Morebits.wiki.addCheckpoint(); if (backlinks) { processunlink(backlinks, false); } if (imageusage) { processunlink(imageusage, true); } Morebits.wiki.removeCheckpoint(); }; Twinkle.unlink.backlinksdone = 0; Twinkle.unlink.imageusagedone = 0; Twinkle.unlink.callbacks = { display: { backlinks: function twinkleunlinkCallbackDisplayBacklinks(apiobj) { var xmlDoc = apiobj.responseXML; var havecontent = false; var list, namespaces, i; if( apiobj.params.image ) { var imageusage = $(xmlDoc).find('query imageusage iu'); list = []; for ( i = 0; i < imageusage.length; ++i ) { var usagetitle = imageusage[i].getAttribute('title'); list.push( { label: usagetitle, value: usagetitle, checked: true } ); } if (!list.length) { apiobj.params.form.append( { type: 'div', label: 'No instances of file usage found.' } ); } else { apiobj.params.form.append( { type:'header', label: 'File usage' } ); namespaces = []; $.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) { namespaces.push(Morebits.wikipedia.namespacesFriendly[v]); }); apiobj.params.form.append( { type: 'div', label: "Selected namespaces: " + namespaces.join(', '), tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]" }); if ($(xmlDoc).find('query-continue').length) { apiobj.params.form.append( { type: 'div', label: "First " + list.length.toString() + " file usages shown." }); } apiobj.params.form.append( { type: 'checkbox', name: 'imageusage', list: list } ); havecontent = true; } } var backlinks = $(xmlDoc).find('query backlinks bl'); if( backlinks.length > 0 ) { list = []; for ( i = 0; i < backlinks.length; ++i ) { var title = backlinks[i].getAttribute('title'); list.push( { label: title, value: title, checked: true } ); } apiobj.params.form.append( { type:'header', label: 'Backlinks' } ); namespaces = []; $.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) { namespaces.push(Morebits.wikipedia.namespacesFriendly[v]); }); apiobj.params.form.append( { type: 'div', label: "Selected namespaces: " + namespaces.join(', '), tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]" }); if ($(xmlDoc).find('query-continue').length) { apiobj.params.form.append( { type: 'div', label: "First " + list.length.toString() + " backlinks shown." }); } apiobj.params.form.append( { type: 'checkbox', name: 'backlinks', list: list }); havecontent = true; } else { apiobj.params.form.append( { type: 'div', label: 'No backlinks found.' } ); } if (havecontent) { apiobj.params.form.append( { type:'submit' } ); } var result = apiobj.params.form.render(); apiobj.params.Window.setContent( result ); } }, unlinkBacklinks: function twinkleunlinkCallbackUnlinkBacklinks(pageobj) { var text, oldtext; text = oldtext = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var wikiPage = new Morebits.wikitext.page(text); wikiPage.removeLink(mw.config.get('wgPageName')); text = wikiPage.getText(); if (text === oldtext) { // Nothing to do, return Twinkle.unlink.callbacks.success(pageobj); Morebits.wiki.actionCompleted(); return; } pageobj.setPageText(text); pageobj.setEditSummary("Removing link(s) to \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd')); pageobj.setCreateOption('nocreate'); pageobj.save(Twinkle.unlink.callbacks.success); }, unlinkImageInstances: function twinkleunlinkCallbackUnlinkImageInstances(pageobj) { var text, oldtext; text = oldtext = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var wikiPage = new Morebits.wikitext.page(text); wikiPage.commentOutImage(mw.config.get('wgTitle'), 'Commented out'); text = wikiPage.getText(); if (text === oldtext) { // Nothing to do, return Twinkle.unlink.callbacks.success(pageobj); Morebits.wiki.actionCompleted(); return; } pageobj.setPageText(text); pageobj.setEditSummary("Commenting out use(s) of file \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd')); pageobj.setCreateOption('nocreate'); pageobj.save(Twinkle.unlink.callbacks.success); }, success: function twinkleunlinkCallbackSuccess(pageobj) { var params = pageobj.getCallbackParameters(); var total = params.total; var now = parseInt( 100 * (params.imageusage ? ++(Twinkle.unlink.imageusagedone) : ++(Twinkle.unlink.backlinksdone))/total, 10 ) + '%'; params.globalstatus.update( now ); if((params.imageusage ? Twinkle.unlink.imageusagedone : Twinkle.unlink.backlinksdone) >= total) { params.globalstatus.info( now + ' (completed)' ); Morebits.wiki.removeCheckpoint(); } } }; /* **************************************** *** twinklewarn.js: Warn module **************************************** * Mode of invocation: Tab ("Warn") * Active on: User talk pages * Config directives in: TwinkleConfig */ Twinkle.warn = function twinklewarn() { if( mw.config.get('wgNamespaceNumber') === 3 ) { //twAddPortletLink( Twinkle.warn.callback, "Warn", "tw-warn", "Warn/notify user" ); } // modify URL of talk page on rollback success pages if( mw.config.get('wgAction') === 'rollback' ) { var $vandalTalkLink = $("#mw-rollback-success").find(".mw-usertoollinks a").first(); $vandalTalkLink.css("font-weight", "bold"); $vandalTalkLink.wrapInner($("<span/>").attr("title", "If appropriate, you can use Twinkle to warn the user about their edits to this page.")); var extraParam = "vanarticle=" + mw.util.rawurlencode(mw.config.get("wgPageName").replace(/_/g, " ")); var href = $vandalTalkLink.attr("href"); if (href.indexOf("?") === -1) { $vandalTalkLink.attr("href", href + "?" + extraParam); } else { $vandalTalkLink.attr("href", href + "&" + extraParam); } } }; Twinkle.warn.callback = function twinklewarnCallback() { if ( !twinkleUserAuthorized ) { alert("Your account is too new to use Twinkle."); return; } if( mw.config.get('wgTitle').split( '/' )[0] === mw.config.get('wgUserName') && !confirm( 'Warning yourself can be seen as a sign of mental instability! Are you sure you want to proceed?' ) ) { return; } var Window = new Morebits.simpleWindow( 600, 440 ); Window.setTitle( "Warn/notify user" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "User talk page warnings", "Template:User_talk_page_warnings#Warnings_and_notices" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#warn" ); var form = new Morebits.quickForm( Twinkle.warn.callback.evaluate ); var main_select = form.append( { type:'field', label:'Choose type of warning/notice to issue', tooltip:'First choose a main warning group, then the specific warning to issue.' } ); var main_group = main_select.append( { type:'select', name:'main_group', event:Twinkle.warn.callback.change_category } ); var defaultGroup = parseInt(Twinkle.getPref('defaultWarningGroup'), 10); main_group.append( { type:'option', label:'General note (1)', value:'level1', selected: ( defaultGroup === 1 || defaultGroup < 1 || ( Morebits.userIsInGroup( 'sysop' ) ? defaultGroup > 8 : defaultGroup > 7 ) ) } ); main_group.append( { type:'option', label:'Caution (2)', value:'level2', selected: ( defaultGroup === 2 ) } ); main_group.append( { type:'option', label:'Warning (3)', value:'level3', selected: ( defaultGroup === 3 ) } ); main_group.append( { type:'option', label:'Final warning (4)', value:'level4', selected: ( defaultGroup === 4 ) } ); main_group.append( { type:'option', label:'Only warning (4im)', value:'level4im', selected: ( defaultGroup === 5 ) } ); main_group.append( { type:'option', label:'Single issue notices', value:'singlenotice', selected: ( defaultGroup === 6 ) } ); main_group.append( { type:'option', label:'Single issue warnings', value:'singlewarn', selected: ( defaultGroup === 7 ) } ); if( Morebits.userIsInGroup( 'sysop' ) ) { main_group.append( { type:'option', label:'Blocking', value:'block', selected: ( defaultGroup === 8 ) } ); } main_select.append( { type:'select', name:'sub_group', event:Twinkle.warn.callback.change_subcategory } ); //Will be empty to begin with. form.append( { type:'input', name:'article', label:'Linked article', value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ), tooltip:'An article can be linked within the notice, perhaps because it was a revert to said article that dispatched this notice. Leave empty for no article to be linked.' } ); var more = form.append( { type: 'field', name: 'reasonGroup', label: 'Warning information' } ); more.append( { type:'textarea', label:'Optional message:', name:'reason', tooltip:'Perhaps a reason, or that a more detailed notice must be appended' } ); var previewlink = document.createElement( 'a' ); $(previewlink).click(function(){ Twinkle.warn.callbacks.preview(result); // |result| is defined below }); previewlink.style.cursor = "pointer"; previewlink.textContent = 'Preview'; more.append( { type: 'div', id: 'warningpreview', label: [ previewlink ] } ); more.append( { type: 'div', id: 'twinklewarn-previewbox', style: 'display: none' } ); more.append( { type:'submit', label:'Submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); result.main_group.root = result; result.previewer = new Morebits.wiki.preview($(result).find('div#twinklewarn-previewbox').last()[0]); // We must init the first choice (General Note); var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.main_group.dispatchEvent( evt ); }; // This is all the messages that might be dispatched by the code // Each of the individual templates require the following information: // label (required): A short description displayed in the dialog // summary (required): The edit summary used. If an article name is entered, the summary is postfixed with "on [[article]]", and it is always postfixed with ". $summaryAd" // suppressArticleInSummary (optional): Set to true to suppress showing the article name in the edit summary. Useful if the warning relates to attack pages, or some such. Twinkle.warn.messages = { level1: { "uw-vandalism1": { label:"Vandalism", summary:"General note: Unhelpful changes" }, "uw-test1": { label:"Editing tests", summary:"General note: Editing tests" }, "uw-delete1": { label:"Removal of content, blanking", summary:"General note: Removal of content, blanking" }, "uw-create1": { label:"Creating inappropriate pages", summary:"General note: Creating inappropriate pages" }, "uw-advert1": { label:"Using Wikipedia for advertising or promotion", summary:"General note: Using Wikipedia for advertising or promotion" }, "uw-copyright1": { label:"Copyright violation", summary:"General note: Violating copyright" }, "uw-error1": { label:"Deliberately adding wrong information", summary:"General note: Adding wrong information" }, "uw-biog1": { label:"Adding unreferenced controversial information about living persons", summary:"General note: Adding unreferenced controversial information about living persons" }, "uw-mos1": { label:"Manual of style", summary:"General note: Formatting, date, language, etc (Manual of style)" }, "uw-move1": { label:"Page moves against naming conventions or consensus", summary:"General note: Page moves against naming conventions or consensus" }, "uw-npov1": { label:"Not adhering to neutral point of view", summary:"General note: Not adhering to neutral point of view" }, "uw-tpv1": { label:"Changing others' talk page comments", summary:"General note: Changing others' talk page comments" }, "uw-qd": { label:"Removing quick-deletion templates", summary:"General note: Removing quick-deletion templates" }, "uw-npa1": { label:"Personal attack directed at another editor", summary:"General note: Personal attack directed at another editor" }, "uw-agf1": { label:"Not assuming good faith", summary:"General note: Not assuming good faith" }, "uw-unsourced1": { label:"Addition of unsourced or improperly cited material", summary:"General note: Addition of unsourced or improperly cited material" } }, level2: { "uw-vandalism2": { label:"Vandalism", summary:"Caution: Vandalism" }, "uw-test2": { label:"Editing tests", summary:"Caution: Editing tests" }, "uw-delete2": { label:"Removal of content, blanking", summary:"Caution: Removal of content, blanking" }, "uw-create2": { label:"Creating inappropriate pages", summary:"Caution: Creating inappropriate pages" }, "uw-advert2": { label:"Using Wikipedia for advertising or promotion", summary:"Caution: Using Wikipedia for advertising or promotion" }, "uw-copyright2": { label:"Copyright violation", summary:"Caution: Violating copyright" }, "uw-npov2": { label:"Not adhering to neutral point of view", summary:"Caution: Not adhering to neutral point of view" }, "uw-error2": { label:"Deliberately adding wrong information", summary:"Caution: Adding wrong information" }, "uw-biog2": { label:"Adding unreferenced controversial information about living persons", summary:"Caution: Adding unreferenced controversial information about living persons" }, "uw-mos2": { label:"Manual of style", summary:"Caution: Formatting, date, language, etc (Manual of style)" }, "uw-move2": { label:"Page moves against naming conventions or consensus", summary:"Caution: Page moves against naming conventions or consensus" }, "uw-tpv2": { label:"Changing others' talk page comments", summary:"Caution: Changing others' talk page comments" }, "uw-npa2": { label:"Personal attack directed at another editor", summary:"Caution: Personal attack directed at another editor" }, "uw-agf2": { label:"Not assuming good faith", summary:"Caution: Not assuming good faith" }, "uw-unsourced2": { label:"Addition of unsourced or improperly cited material", summary:"Caution: Addition of unsourced or improperly cited material" } }, level3: { "uw-vandalism3": { label:"Vandalism", summary:"Warning: Vandalism" }, "uw-test3": { label:"Editing tests", summary:"Warning: Editing tests" }, "uw-delete3": { label:"Removal of content, blanking", summary:"Warning: Removal of content, blanking" }, "uw-create3": { label:"Creating inappropriate pages", summary:"Warning: Creating inappropriate pages" }, "uw-advert3": { label:"Using Wikipedia for advertising or promotion", summary:"Warning: Using Wikipedia for advertising or promotion" }, "uw-npov3": { label:"Not adhering to neutral point of view", summary:"Warning: Not adhering to neutral point of view" }, "uw-error3": { label:"Deliberately adding wrong information", summary:"Warning: Adding wrong information" }, "uw-biog3": { label:"Adding unreferenced controversial or defamatory information about living persons", summary:"Warning: Adding unreferenced controversial information about living persons" }, "uw-mos3": { label:"Manual of style", summary:"Warning: Formatting, date, language, etc (Manual of style)" }, "uw-move3": { label:"Page moves against naming conventions or consensus", summary:"Warning: Page moves against naming conventions or consensus" }, "uw-tpv3": { label:"Changing others' talk page comments", summary:"Warning: Changing others' talk page comments" }, "uw-npa3": { label:"Personal attack directed at another editor", summary:"Warning: Personal attack directed at another editor" }, "uw-agf3": { label:"Not assuming good faith", summary:"Warning: Not assuming good faith" } }, level4: { "uw-generic4": { label:"Generic warning (for template series missing level 4)", summary:"Final warning notice" }, "uw-vandalism4": { label:"Vandalism", summary:"Final warning: Vandalism" }, "uw-test4": { label:"Editing tests", summary:"Final warning: Editing tests" }, "uw-delete4": { label:"Removal of content, blanking", summary:"Final warning: Removal of content, blanking" }, "uw-create4": { label:"Creating inappropriate pages", summary:"Final warning: Creating inappropriate pages" }, "uw-advert4": { label:"Using Wikipedia for advertising or promotion", summary:"Final warning: Using Wikipedia for advertising or promotion" }, "uw-npov4": { label:"Not adhering to neutral point of view", summary:"Final warning: Not adhering to neutral point of view" }, "uw-error4": { label:"Deliberately adding wrong information", summary:"Final Warning: Adding wrong information" }, "uw-biog4": { label:"Adding unreferenced defamatory information about living persons", summary:"Final warning: Adding unreferenced controversial information about living persons" }, "uw-mos4": { label:"Manual of style", summary:"Final warning: Formatting, date, language, etc (Manual of style)" }, "uw-move4": { label:"Page moves against naming conventions or consensus", summary:"Final warning: Page moves against naming conventions or consensus" }, "uw-npa4": { label:"Personal attack directed at another editor", summary:"Final warning: Personal attack directed at another editor" } }, level4im: { "uw-vandalism4im": { label:"Vandalism", summary:"Only warning: Vandalism" }, "uw-delete4im": { label:"Removal of content, blanking", summary:"Only warning: Removal of content, blanking" }, "uw-create4im": { label:"Creating inappropriate pages", summary:"Only warning: Creating inappropriate pages" }, "uw-biog4im": { label:"Adding unreferenced defamatory information about living persons", summary:"Only warning: Adding unreferenced controversial information about living persons" }, "uw-move4im": { label:"Page moves against naming conventions or consensus", summary:"Only warning: Page moves against naming conventions or consensus" }, "uw-npa4im": { label:"Personal attack directed at another editor", summary:"Only warning: Personal attack directed at another editor" } }, singlenotice: { "uw-badcat": { label:"Adding incorrect categories", summary:"Notice: Adding incorrect categories" }, "uw-bite": { label:"\"Biting\" newcomers", summary:"Notice: \"Biting\" newcomers" }, "uw-coi": { label:"Possible conflict of interest", summary:"Notice: Possible conflict of interest" }, "uw-encopypaste": { label:"Direct copying of article from English Wikipedia", summary:"Notice: Direct copying of article from English Wikipedia" }, "uw-encopyright": { label:"Not giving attribution for content from another Wikipedia", summary:"Notice: Reusing content from English Wikipedia without attribution" }, "uw-emptycat": { label:"Category created does not contain enough pages", summary:"Notice: Creating empty categories" }, "uw-joke": { label:"Using improper humor", summary:"Notice: Using improper humor" }, "uw-lang": { label:"Changing between types of English without a good reason", summary:"Notice: Unnecessarily changing between British and American English" }, "uw-newarticle": { label:"Tips on creating new articles", summary:"Notice: How to make your articles better" }, "uw-notenglish": { label:"Changes not in English", summary:"Notice: Please edit in English" }, "uw-otherweb": { label:"Use \"Other websites\", not \"External links\"", summary:"Notice: Use \"Other websites\", not \"External links\"" }, "uw-sandbox": { label:"Removing the sandbox header", summary:"Notice: Do not remove sandbox header" }, "uw-selfrevert": { label:"Undoing recent test", summary:"Notice: Undoing recent test" }, "uw-simple": { label:"Not making changes in simple English", summary:"Notice: Not making changes in simple English" }, "uw-spellcheck": { label:"Review spelling, etc.", summary:"Notice: Review spelling, etc." }, "uw-subst": { label:"Remember to subst: templates", summary:"Notice: Remember to subst: templates" }, "uw-tilde": { label:"Not signing posts", summary:"Notice: Not signing posts" }, "uw-upload": { label:"Image uploads not allowed in Simple English Wikipedia", summary:"Notice: Image uploads not allowed in Simple English Wikipedia" }, "uw-warn": { label:"Use user warn templates", summary:"Notice: Use user warn templates" } }, singlewarn: { "uw-3rr": { label:"Edit warring", summary:"Warning: Involved in edit war" }, "uw-attack": { label:"Creating attack pages", summary:"Warning: Creating attack pages" }, "uw-cyberbully": { label:"Cyberbullying", summary:"Warning: Cyberbullying" }, "uw-disruption": { label:"Project disruption", summary:"Warning: Project disruption" }, "uw-longterm": { label:"Long term abuse", summary:"Warning: Long term abuse" }, "uw-qd": { label:"Removing quick deletion templates from articles", summary:"Warning: Removing quick deletion templates from articles" }, "uw-spam": { label:"Adding spam links", summary:"Warning: Adding spam links" }, "uw-userpage": { label:"Userpage or subpage is against policy", summary:"Warning: Userpage or subpage is against policy" } }, block: { "uw-block1": { label: "Block level 1", summary: "You have been temporarily blocked", reasonParam: true }, "uw-block2": { label: "Block level 2", summary: "You have been blocked", reasonParam: true }, "uw-block3": { label: "Block level 3", summary: "You have been indefinitely blocked", reasonParam: true }, "UsernameBlocked": { label: "Username block", summary: "You have been blocked for violation of the [[Wikipedia:Username|username policy]]", reasonParam: true }, "UsernameHardBlocked": { label: "Username hard block", summary: "You have been blocked for a blatant violation of the [[Wikipedia:Username|username policy]]", reasonParam: true }, "Blocked proxy": { label: "Blocked proxy", summary: "You have been blocked because this IP is an [[open proxy]]" }, "Uw-spamblock": { label: "Spam block", summary: "You have been blocked for [[Wikipedia:Spam|advertising or promotion]]" }, "Cyberbully block": { label: "Cyberbully block", summary: "You have been blocked for [[Wikipedia:Cyberbullying|cyberbullying]]" }, "Talkpage-revoked": { label: "Talk-page access removed", summary: "Your ability to change this [[Wikipedia:Talk page|talk page]] has been removed" } } }; Twinkle.warn.prev_block_timer = null; Twinkle.warn.prev_block_reason = null; Twinkle.warn.prev_article = null; Twinkle.warn.prev_reason = null; Twinkle.warn.callback.change_category = function twinklewarnCallbackChangeCategory(e) { var value = e.target.value; var sub_group = e.target.root.sub_group; var messages = Twinkle.warn.messages[ value ]; sub_group.main_group = value; var old_subvalue = sub_group.value; var old_subvalue_re; if( old_subvalue ) { old_subvalue = old_subvalue.replace(/\d*(im)?$/, '' ); old_subvalue_re = new RegExp( RegExp.escape( old_subvalue ) + "(\\d*(?:im)?)$" ); } while( sub_group.hasChildNodes() ){ sub_group.removeChild( sub_group.firstChild ); } for( var i in messages ) { var selected = false; if( old_subvalue && old_subvalue_re.test( i ) ) { selected = true; } var elem = new Morebits.quickForm.element( { type:'option', label:"{{" + i + "}}: " + messages[i].label, value:i, selected: selected } ); sub_group.appendChild( elem.render() ); } if( value === 'block' ) { // create the block-related fields var more = new Morebits.quickForm.element( { type: 'div', id: 'block_fields' } ); more.append( { type: 'input', name: 'block_timer', label: 'Period of blocking / Host ', tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc... If you selected "blocked proxy", this text box will append the host name of the server' } ); more.append( { type: 'input', name: 'block_reason', label: '"You have been blocked for ..." ', tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.' } ); e.target.root.insertBefore( more.render(), e.target.root.lastChild ); // restore saved values of fields if(Twinkle.warn.prev_block_timer !== null) { e.target.root.block_timer.value = Twinkle.warn.prev_block_timer; Twinkle.warn.prev_block_timer = null; } if(Twinkle.warn.prev_block_reason !== null) { e.target.root.block_reason.value = Twinkle.warn.prev_block_reason; Twinkle.warn.prev_block_reason = null; } if(Twinkle.warn.prev_article === null) { Twinkle.warn.prev_article = e.target.root.article.value; } e.target.root.article.disabled = false; $(e.target.root.reason).parent().hide(); e.target.root.previewer.closePreview(); } else if( e.target.root.block_timer ) { // hide the block-related fields if(!e.target.root.block_timer.disabled && Twinkle.warn.prev_block_timer === null) { Twinkle.warn.prev_block_timer = e.target.root.block_timer.value; } if(!e.target.root.block_reason.disabled && Twinkle.warn.prev_block_reason === null) { Twinkle.warn.prev_block_reason = e.target.root.block_reason.value; } $(e.target.root).find("#block_fields").remove(); if(e.target.root.article.disabled && Twinkle.warn.prev_article !== null) { e.target.root.article.value = Twinkle.warn.prev_article; Twinkle.warn.prev_article = null; } e.target.root.article.disabled = false; $(e.target.root.reason).parent().show(); e.target.root.previewer.closePreview(); } // clear overridden label on article textbox Morebits.quickForm.setElementTooltipVisibility(e.target.root.article, true); Morebits.quickForm.resetElementLabel(e.target.root.article); }; Twinkle.warn.callback.change_subcategory = function twinklewarnCallbackChangeSubcategory(e) { var main_group = e.target.form.main_group.value; var value = e.target.form.sub_group.value; if( main_group === 'singlewarn' ) { if( value === 'uw-username' ) { if(Twinkle.warn.prev_article === null) { Twinkle.warn.prev_article = e.target.form.article.value; } e.target.form.article.notArticle = true; e.target.form.article.value = ''; } else if( e.target.form.article.notArticle ) { if(Twinkle.warn.prev_article !== null) { e.target.form.article.value = Twinkle.warn.prev_article; Twinkle.warn.prev_article = null; } e.target.form.article.notArticle = false; } } else if( main_group === 'block' ) { if( Twinkle.warn.messages.block[value].indefinite ) { if(Twinkle.warn.prev_block_timer === null) { Twinkle.warn.prev_block_timer = e.target.form.block_timer.value; } e.target.form.block_timer.disabled = true; e.target.form.block_timer.value = 'indefinite'; } else if( e.target.form.block_timer.disabled ) { if(Twinkle.warn.prev_block_timer !== null) { e.target.form.block_timer.value = Twinkle.warn.prev_block_timer; Twinkle.warn.prev_block_timer = null; } e.target.form.block_timer.disabled = false; } if( Twinkle.warn.messages.block[value].pageParam ) { if(Twinkle.warn.prev_article !== null) { e.target.form.article.value = Twinkle.warn.prev_article; Twinkle.warn.prev_article = null; } e.target.form.article.disabled = false; } else if( !e.target.form.article.disabled ) { if(Twinkle.warn.prev_article === null) { Twinkle.warn.prev_article = e.target.form.article.value; } e.target.form.article.disabled = true; e.target.form.article.value = ''; } if( Twinkle.warn.messages.block[value].reasonParam ) { if(Twinkle.warn.prev_block_reason !== null) { e.target.form.block_reason.value = Twinkle.warn.prev_block_reason; Twinkle.warn.prev_block_reason = null; } e.target.form.block_reason.disabled = false; } else if( !e.target.form.block_reason.disabled ) { if(Twinkle.warn.prev_block_reason === null) { Twinkle.warn.prev_block_reason = e.target.form.block_reason.value; } e.target.form.block_reason.disabled = true; e.target.form.block_reason.value = ''; } } // change form labels according to the warning selected if (value === "uw-username") { Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, false); Morebits.quickForm.overrideElementLabel(e.target.form.article, "Username violates policy because... "); } else { Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, true); Morebits.quickForm.resetElementLabel(e.target.form.article); } }; Twinkle.warn.callbacks = { preview: function(form) { var templatename = form.sub_group.value; var templatetext = '{{subst:' + templatename; var linkedarticle = form.article.value; if (templatename in Twinkle.warn.messages.block) { if( linkedarticle && Twinkle.warn.messages.block[templatename].pageParam ) { templatetext += '|page=' + linkedarticle; } var blocktime = form.block_timer.value; if( /te?mp|^\s*$|min/.exec( blocktime ) || Twinkle.warn.messages.block[templatename].indefinite ) { ; // nothing } else if( /indef|\*|max/.exec( blocktime ) ) { templatetext += '|indef=yes'; } else { templatetext += '|host=' + blocktime; templatetext += '|time=' + blocktime; } var blockreason = form.block_reason.value; if( blockreason ) { templatetext += '|reason=' + blockreason; } templatetext += "|sig=true}}"; } else { if (linkedarticle) { // add linked article for user warnings (non-block templates) templatetext += '|1=' + linkedarticle; } templatetext += '}}'; // add extra message for non-block templates var reason = form.reason.value; if (reason) { templatetext += " ''" + reason + "''"; } } form.previewer.beginRender(templatetext); }, main: function( pageobj ) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var messageData = Twinkle.warn.messages[params.main_group][params.sub_group]; var history_re = /<!-- Template:(uw-.*?) -->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4}) \(UTC\)/g; var history = {}; var latest = { date:new Date( 0 ), type:'' }; var current; while( ( current = history_re.exec( text ) ) ) { var current_date = new Date( current[2] + ' UTC' ); if( !( current[1] in history ) || history[ current[1] ] < current_date ) { history[ current[1] ] = current_date; } if( current_date > latest.date ) { latest.date = current_date; latest.type = current[1]; } } var date = new Date(); if( params.sub_group in history ) { var temp_time = new Date( history[ params.sub_group ] ); temp_time.setUTCHours( temp_time.getUTCHours() + 24 ); if( temp_time > date ) { if( !confirm( "An identical " + params.sub_group + " has been issued in the last 24 hours. \nWould you still like to add this warning/notice?" ) ) { pageobj.statelem.info( 'aborted per user request' ); return; } } } latest.date.setUTCMinutes( latest.date.getUTCMinutes() + 1 ); // after long debate, one minute is max if( latest.date > date ) { if( !confirm( "A " + latest.type + " has been issued in the last minute. \nWould you still like to add this warning/notice?" ) ) { pageobj.statelem.info( 'aborted per user request' ); return; } } var mainheaderRe = new RegExp("==+\\s*Warnings\\s*==+"); var headerRe = new RegExp( "^==+\\s*(?:" + date.getUTCMonthName() + '|' + date.getUTCMonthNameAbbrev() + ")\\s+" + date.getUTCFullYear() + "\\s*==+", 'm' ); if( text.length > 0 ) { text += "\n\n"; } if( params.main_group === 'block' ) { var article = '', reason = '', host = '', time = null; if( Twinkle.getPref('blankTalkpageOnIndefBlock') && params.sub_group !== 'uw-lblock' && ( Twinkle.warn.messages.block[params.sub_group].indefinite || (/indef|\*|max/).exec( params.block_timer ) ) ) { Morebits.status.info( 'Info', 'Blanking talk page per preferences and creating a new level 2 heading for the date' ); text = "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n"; } else if( !headerRe.exec( text ) ) { Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' ); text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n"; } if( params.reason && Twinkle.warn.messages.block[params.sub_group].reasonParam ) { reason = '|reason=' + params.reason; } if( /te?mp|^\s*$|min/.exec( params.block_timer ) || Twinkle.warn.messages.block[params.sub_group].indefinite ) { time = ''; } else if( /indef|\*|max/.exec( params.block_timer ) ) { time = '|indef=yes'; } else { time = '|time=' + params.block_timer; } if ( params.sub_group === "Blocked proxy" ) { text += "{{" + params.sub_group + "|host=" + params.block_timer + "}}"; } else { text += "{{subst:" + params.sub_group + time + reason + "|sig=yes}}"; } } else { if( !headerRe.exec( text ) ) { Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' ); text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n"; } text += "{{subst:" + params.sub_group + ( params.article ? '|1=' + params.article : '' ) + "|subst=subst:}}" + (params.reason ? " ''" + params.reason + "'' ": ' ' ) + "~~~~"; } if ( Twinkle.getPref('showSharedIPNotice') && Morebits.isIPAddress( mw.config.get('wgTitle') ) ) { Morebits.status.info( 'Info', 'Adding a shared IP notice' ); text += "\n{{subst:SharedIPAdvice}}"; } var summary = messageData.summary; if ( messageData.suppressArticleInSummary !== true && params.article ) { summary += " on [[" + params.article + "]]"; } summary += "." + Twinkle.getPref("summaryAd"); pageobj.setPageText( text ); pageobj.setEditSummary( summary ); pageobj.setWatchlist( Twinkle.getPref('watchWarnings') ); pageobj.save(); } }; Twinkle.warn.callback.evaluate = function twinklewarnCallbackEvaluate(e) { // First, check to make sure a reason was filled in if uw-username was selected if(e.target.sub_group.value === 'uw-username' && e.target.article.value.trim() === '') { alert("You must supply a reason for the {{uw-username}} template."); return; } // Then, grab all the values provided by the form var params = { reason: e.target.block_reason ? e.target.block_reason.value : e.target.reason.value, main_group: e.target.main_group.value, sub_group: e.target.sub_group.value, article: e.target.article.value, // .replace( /^(Image|Category):/i, ':$1:' ), -- apparently no longer needed... block_timer: e.target.block_timer ? e.target.block_timer.value : null }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Warning complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page( mw.config.get('wgPageName'), 'User talk page modification' ); wikipedia_page.setCallbackParameters( params ); wikipedia_page.setFollowRedirect( true ); wikipedia_page.load( Twinkle.warn.callbacks.main ); }; /* **************************************** *** twinklexfd.js: XFD module **************************************** * Mode of invocation: Tab ("XFD") * Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects * Config directives in: TwinkleConfig */ Twinkle.xfd = function twinklexfd() { // Disable on: // * special pages // * non-existent pages // * files on Commons, whether there is a local page or not (unneeded local pages of files on Commons are eligible for CSD F2) // * file pages without actual files (these are eligible for CSD G8) if ( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') || (mw.config.get('wgNamespaceNumber') === 6 && (document.getElementById('mw-sharedupload') || (!document.getElementById('mw-imagepage-section-filehistory') && !Morebits.wiki.isPageRedirect()))) ) { return; } //twAddPortletLink( Twinkle.xfd.callback, "RfD", "tw-xfd", "Nominate for deletion" ); }; Twinkle.xfd.num2order = function twinklexfdNum2order( num ) { switch( num ) { case 1: return ''; case 2: return '2nd'; case 3: return '3rd'; default: return num + 'th'; } }; Twinkle.xfd.currentRationale = null; // error callback on Morebits.status.object Twinkle.xfd.printRationale = function twinklexfdPrintRationale() { if (Twinkle.xfd.currentRationale) { var p = document.createElement("p"); p.textContent = "Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:"; var pre = document.createElement("pre"); pre.className = "toccolours"; pre.style.marginTop = "0"; pre.textContent = Twinkle.xfd.currentRationale; p.appendChild(pre); Morebits.status.root.appendChild(p); // only need to print the rationale once Twinkle.xfd.currentRationale = null; } }; Twinkle.xfd.callback = function twinklexfdCallback() { if (!twinkleUserAuthorized) { alert("Your account is too new to use Twinkle."); return; } var Window = new Morebits.simpleWindow( 600, 350 ); Window.setTitle( "Nominate for deletion (RfD)" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Deletion policy", "Wikipedia:Deletion policy" ); Window.addFooterLink( "About deletion discussions", "WP:RfD" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#xfd" ); var form = new Morebits.quickForm( Twinkle.xfd.callback.evaluate ); var categories = form.append( { type: 'select', name: 'category', label: 'Select wanted type of category: ', tooltip: 'This default should be the most appropriate, as no other deletion discussion pages exist here.', event: Twinkle.xfd.callback.change_category } ); categories.append( { type: 'option', label: 'RfD (Requests for deletion)', selected: true, value: 'afd' } ); form.append( { type: 'checkbox', list: [ { label: 'Notify page creator if possible', value: 'notify', name: 'notify', tooltip: "A notification template will be placed on the creator's talk page if this is true.", checked: true } ] } ); form.append( { type: 'field', label:'Work area', name: 'work_area' } ); form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); // We must init the controls var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.category.dispatchEvent( evt ); }; Twinkle.xfd.previousNotify = true; Twinkle.xfd.callback.change_category = function twinklexfdCallbackChangeCategory(e) { var value = e.target.value; var form = e.target.form; var old_area = Morebits.quickForm.getElements(e.target.form, "work_area")[0]; var work_area = null; var oldreasontextbox = form.getElementsByTagName('textarea')[0]; var oldreason = (oldreasontextbox ? oldreasontextbox.value : ''); work_area = new Morebits.quickForm.element( { type: 'field', label: 'Requests for deletion', name: 'work_area' } ); work_area.append( { type: 'checkbox', list: [ { label: 'Wrap deletion tag with <noinclude>', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; tags, so that it won\'t transclude. This option is not normally required.' } ] } ); work_area.append( { type: 'textarea', name: 'xfdreason', label: 'Reason: ' } ); work_area = work_area.render(); old_area.parentNode.replaceChild( work_area, old_area ); } Twinkle.xfd.callbacks = { afd: { main: function(apiobj) { var xmlDoc = apiobj.responseXML; var titles = $(xmlDoc).find('allpages p'); // There has been no earlier entries with this prefix, just go on. if( titles.length <= 0 ) { apiobj.params.numbering = apiobj.params.number = ''; } else { var number = 0; for( var i = 0; i < titles.length; ++i ) { var title = titles[i].getAttribute('title'); // First, simple test, is there an instance with this exact name? if( title === 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') ) { number = Math.max( number, 1 ); continue; } var order_re = new RegExp( '^' + RegExp.escape( 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'), true ) + '\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$'); var match = order_re.exec( title ); // No match; A non-good value if( !match ) { continue; } // A match, set number to the max of current number = Math.max( number, Number(match[1]) ); } apiobj.params.number = Twinkle.xfd.num2order( parseInt( number, 10 ) + 1); apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : ''; } apiobj.params.discussionpage = 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + apiobj.params.numbering; Morebits.status.info( "Next discussion page", "[[" + apiobj.params.discussionpage + "]]" ); // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage; Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to the discussion page"; // Tagging article var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Adding deletion tag to article"); if(window.location.search.includes("redirect=no")) { wikipedia_page.setFollowRedirect(false); // User's intention was probably to tag the redirect itself } else { wikipedia_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect } wikipedia_page.setCallbackParameters(apiobj.params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle); }, // Tagging needs to happen before everything else: this means we can check if there is an AfD tag already on the page taggingArticle: function(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var statelem = pageobj.getStatusElement(); // Check for existing AfD tag, for the benefit of new page patrollers var textNoAfd = text.replace(/\{\{\s*(Requests for deletion\/dated|RfDM)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, ""); if (text !== textNoAfd) { if (confirm("An RfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current RfD tag (not recommended), or Cancel to abandon your nomination.")) { text = textNoAfd; } else { statelem.error("Article already tagged with RfD tag, and you chose to abort"); window.location.reload(); return; } } // Now we know we want to go ahead with it, trigger the other AJAX requests // Starting discussion page var wikipedia_page = new Morebits.wiki.page(params.discussionpage, "Creating article deletion discussion page"); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage); // Today's list var date = new Date(); wikipedia_page = new Morebits.wiki.page('Wikipedia:Requests for deletion', "Adding discussion to today's list"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList); // Notification to first contributor if (params.usertalk) { var thispage = new Morebits.wiki.page(mw.config.get('wgPageName')); thispage.setCallbackParameters(params); thispage.lookupCreator(Twinkle.xfd.callbacks.afd.userNotification); } // Then, test if there are speedy deletion-related templates on the article. var textNoSd = text.replace(/\{\{\s*(db(-\w*)?|qd|delete|(?:hang|hold)[\- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, ""); if (text !== textNoSd && confirm("A quick deletion tag was found on this page. Should it be removed?")) { text = textNoSd; } pageobj.setPageText(( params.noinclude ? "<noinclude>" : "" ) + "\{\{RfD|" + params.reason + "\}\}\n" + ( params.noinclude ? "</noinclude>" : "" ) + text); pageobj.setEditSummary("Nominated for deletion; see [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd')); switch (Twinkle.getPref('xfdWatchPage')) { case 'yes': pageobj.setWatchlist(true); break; case 'no': pageobj.setWatchlistFromPreferences(false); break; default: pageobj.setWatchlistFromPreferences(true); break; } pageobj.setCreateOption('nocreate'); pageobj.save(); }, discussionPage: function(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); pageobj.setPageText("{{subst:RfD/Preload/Template|deletereason=" + params.reason + "}}\n"); pageobj.setEditSummary("Creating deletion discussion page for [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); switch (Twinkle.getPref('xfdWatchDiscussion')) { case 'yes': pageobj.setWatchlist(true); break; case 'no': pageobj.setWatchlistFromPreferences(false); break; default: pageobj.setWatchlistFromPreferences(true); break; } pageobj.setCreateOption('createonly'); pageobj.save(function() { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); }, todaysList: function(pageobj) { var old_text = pageobj.getPageText() + "\n"; // MW strips trailing blanks, but we like them, so we add a fake one var params = pageobj.getCallbackParameters(); var statelem = pageobj.getStatusElement(); var text = old_text.replace( /(<\!-- Add new entries to the TOP of the following list -->\n+)/, "$1{{Wikipedia:Requests for deletion/Requests/" + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + params.numbering + "}}\n"); if( text === old_text ) { statelem.error( 'failed to find target spot for the discussion' ); return; } pageobj.setPageText(text); pageobj.setEditSummary("Adding [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd')); switch (Twinkle.getPref('xfdWatchList')) { case 'yes': pageobj.setWatchlist(true); break; case 'no': pageobj.setWatchlistFromPreferences(false); break; default: pageobj.setWatchlistFromPreferences(true); break; } pageobj.setCreateOption('recreate'); pageobj.save(); }, userNotification: function(pageobj) { var params = pageobj.getCallbackParameters(); var initialContrib = pageobj.getCreator(); var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")"); var notifytext = "\n{{subst:RFDNote|1=" + mw.config.get('wgPageName') + "|2=" + mw.config.get('wgPageName') + ( params.numbering !== '' ? '|order=&#32;' + params.numbering : '' ) + "}} ~~~~"; usertalkpage.setAppendText(notifytext); usertalkpage.setEditSummary("Notification: listing at [[WP:RfD|requests for deletion]] of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); usertalkpage.setCreateOption('recreate'); switch (Twinkle.getPref('xfdWatchUser')) { case 'yes': usertalkpage.setWatchlist(true); break; case 'no': usertalkpage.setWatchlistFromPreferences(false); break; default: usertalkpage.setWatchlistFromPreferences(true); break; } usertalkpage.setFollowRedirect(true); usertalkpage.append(); } } }; Twinkle.xfd.callback.evaluate = function(e) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var type = e.target.category.value; var usertalk = e.target.notify.checked; var reason = e.target.xfdreason.value; var xfdtarget, xfdtarget2, puf, noinclude, tfdinline, notifyuserspace; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Twinkle.xfd.currentRationale = reason; Morebits.status.onError(Twinkle.xfd.printRationale); if( !type ) { Morebits.status.error( 'Error', 'no action given' ); return; } var query, wikipedia_page, wikipedia_api, logpage, params; var date = new Date(); query = { 'action': 'query', 'list': 'allpages', 'apprefix': 'Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'), 'apnamespace': 4, 'apfilterredir': 'nonredirects', 'aplimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 }; wikipedia_api = new Morebits.wiki.api( 'Tagging article with deletion tag', query, Twinkle.xfd.callbacks.afd.main ); wikipedia_api.params = { usertalk:usertalk, reason:reason, noinclude:noinclude }; wikipedia_api.post(); }; /** * General initialization code */ var scriptpathbefore = "https://dev.miraheze.org" + mw.util.wikiScript( "index" ) + "?title=", scriptpathafter = "&action=raw&ctype=text/javascript&happy=yes"; // Retrieve the user's Twinkle preferences $.ajax({ url: scriptpathbefore + "User:" + encodeURIComponent( mw.config.get("wgUserName")) + "/twinkleoptions.js" + scriptpathafter, dataType: "text", error: function () { mw.notify( "Could not load twinkleoptions.js" ); }, success: function ( optionsText ) { // Quick pass if user has no options if ( optionsText === "" ) { return; } // Twinkle options are basically a JSON object with some comments. Strip those: optionsText = optionsText.replace( /(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, "" ); // First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line. if ( optionsText.lastIndexOf( "window.Twinkle.prefs = ", 0 ) === 0 ) { optionsText = optionsText.replace( /(?:^window.Twinkle.prefs = |;\n*$)/g, "" ); } try { var options = JSON.parse( optionsText ); // Assuming that our options evolve, we will want to transform older versions: //if ( options.optionsVersion === undefined ) { // ... // options.optionsVersion = 1; //} //if ( options.optionsVersion === 1 ) { // ... // options.optionsVersion = 2; //} // At the same time, twinkleconfig.js needs to be adapted to write a higher version number into the options. if ( options ) { Twinkle.prefs = options; } } catch ( e ) { mw.notify("Could not parse twinkleoptions.js"); } }, complete: function () { $( Twinkle.load ); } }); // Developers: you can import custom Twinkle modules here // For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter); Twinkle.load = function () { // Don't activate on special pages other than "Contributions" so that they load faster, especially the watchlist. // Also, Twinkle is incompatible with Internet Explorer versions 8 or lower, so don't load there either. var specialPageWhitelist = [ 'Block', 'Contributions', 'Recentchanges', 'Recentchangeslinked' ]; // wgRelevantUserName defined for non-sysops on Special:Block if (Morebits.userIsInGroup('sysop')) { specialPageWhitelist = specialPageWhitelist.concat([ 'DeletedContributions', 'Prefixindex' ]); } if (mw.config.get('wgNamespaceNumber') === -1 && specialPageWhitelist.indexOf(mw.config.get('wgCanonicalSpecialPageName')) === -1) { return; } // Prevent clickjacking if (window.top !== window.self) { return; } if ($.client.profile().name === 'msie' && $.client.profile().versionNumber < 9) { return; } // Set custom Api-User-Agent header, for server-side logging purposes Morebits.wiki.api.setApiUserAgent('Twinkle/2.0 (' + mw.config.get('wgDBname') + ')'); // Load the modules in the order that the tabs should appears // Deletion Twinkle.speedy(); // Misc. ones last Twinkle.diff(); Twinkle.unlink(); Twinkle.config.init(); Twinkle.fluff.init(); if ( Morebits.userIsInGroup('sysop') ) { Twinkle.batchdelete(); Twinkle.batchprotect(); Twinkle.batchundelete(); } // Run the initialization callbacks for any custom modules $( Twinkle.initCallbacks ).each(function ( k, v ) { v(); }); Twinkle.addInitCallback = function ( func ) { func(); }; // Increases text size in Twinkle dialogs, if so configured if ( Twinkle.getPref( "dialogLargeFont" ) ) { mw.util.addCSS( ".morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } " + ".morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }" ); } }; } ( window, document, jQuery )); // End wrap with anonymous function // </nowiki> 3399804aac47c3b253525d584e1077068e37daf4 MediaWiki:Gadget-morebits.js 8 82 156 2023-03-11T01:50:47Z dev>Pppery 0 javascript text/javascript // <nowiki> /** * morebits.js * =========== * A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia. * * The highlights include: * - Morebits.quickForm class - generates quick HTML forms on the fly * - Morebits.wiki.api class - makes calls to the MediaWiki API * - Morebits.wiki.page class - modifies pages on the wiki (edit, revert, delete, etc.) * - Morebits.wikitext class - contains some utilities for dealing with wikitext * - Morebits.status class - a rough-and-ready status message displayer, used by the Morebits.wiki classes * - Morebits.simpleWindow class - a wrapper for jQuery UI Dialog with a custom look and extra features * * Dependencies: * - The whole thing relies on jQuery. But most wikis should provide this by default. * - Morebits.quickForm, Morebits.simpleWindow, and Morebits.status rely on the "morebits.css" file for their styling. * - Morebits.simpleWindow relies on jquery UI Dialog (ResourceLoader module name 'jquery.ui'). * - Morebits.quickForm tooltips rely on Tipsy (ResourceLoader module name 'jquery.tipsy'). * For external installations, Tipsy is available at [http://onehackoranother.com/projects/jquery/tipsy]. * - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition: * * GadgetName[ResourceLoader|dependencies=mediawiki.util,jquery.ui,jquery.tipsy]|morebits.js|morebits.css|GadgetName.js * * Most of the stuff here doesn't work on IE < 9. It is your script's responsibility to enforce this. * * This library is maintained by the maintainers of Twinkle. * For queries, suggestions, help, etc., head to [[Wikipedia talk:Twinkle]] on English Wikipedia [http://en.wikipedia.org]. * The latest development source is available at [https://github.com/azatoth/twinkle/blob/master/morebits.js]. * * From simplewiki */ ( function ( window, document, $, undefined ) { // Wrap entire file with anonymous function var Morebits = {}; window.Morebits = Morebits; // allow global access /** * **************** Morebits.userIsInGroup() **************** * Simple helper function to see what groups a user might belong */ Morebits.userIsInGroup = function ( group ) { return $.inArray(group, mw.config.get( 'wgUserGroups' )) !== -1; } /** * **************** Morebits.isIPAddress() **************** * Helper function: Returns true if given string contains a valid IPv4 or * IPv6 address */ Morebits.isIPAddress = function ( address ) { return mw.util.isIPv4Address(address) || mw.util.isIPv6Address(address); }; /** * **************** Morebits.sanitizeIPv6() **************** * JavaScript translation of the MediaWiki core function IP::sanitizeIP() in * includes/utils/IP.php. * Converts an IPv6 address to the canonical form stored and used by MediaWiki. */ Morebits.sanitizeIPv6 = function ( address ) { address = address.trim(); if ( address === '' ) { return null; } if ( mw.util.isIPv4Address( address ) || !mw.util.isIPv6Address( address ) ) { return address; // nothing else to do for IPv4 addresses or invalid ones } // Remove any whitespaces, convert to upper case address = address.toUpperCase(); // Expand zero abbreviations var abbrevPos = address.indexOf( '::' ); if ( abbrevPos > -1 ) { // We know this is valid IPv6. Find the last index of the // address before any CIDR number (e.g. "a:b:c::/24"). var CIDRStart = address.indexOf( '/' ); var addressEnd = ( CIDRStart > -1 ) ? CIDRStart - 1 : address.length - 1; // If the '::' is at the beginning... var repeat, extra, pad; if ( abbrevPos === 0 ) { repeat = '0:'; extra = ( address == '::' ) ? '0' : ''; // for the address '::' pad = 9; // 7+2 (due to '::') // If the '::' is at the end... } else if ( abbrevPos === ( addressEnd - 1 ) ) { repeat = ':0'; extra = ''; pad = 9; // 7+2 (due to '::') // If the '::' is in the middle... } else { repeat = ':0'; extra = ':'; pad = 8; // 6+2 (due to '::') } var replacement = repeat; pad -= address.split( ':' ).length - 1; for ( var i = 1; i < pad; i++ ) { replacement += repeat; } replacement += extra; address = address.replace( '::', replacement ); } // Remove leading zeros from each bloc as needed address = address.replace( /(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2' ); return address; }; /** * **************** Morebits.quickForm **************** * Morebits.quickForm is a class for creation of simple and standard forms without much * specific coding. * * Index to Morebits.quickForm element types: * * select A combo box (aka drop-down). * - Attributes: name, label, multiple, size, list, event * option An element for a combo box. * - Attributes: value, label, selected, disabled * optgroup A group of "option"s. * - Attributes: label, list * field A fieldset (aka group box). * - Attributes: name, label * checkbox A checkbox. Must use "list" parameter. * - Attributes: name, list, event * - Attributes (within list): name, label, value, checked, disabled, event, subgroup * radio A radio button. Must use "list" parameter. * - Attributes: name, list, event * - Attributes (within list): name, label, value, checked, disabled, event, subgroup * input A text box. * - Attributes: name, label, value, size, disabled, readonly, maxlength, event * dyninput A set of text boxes with "Remove" buttons and an "Add" button. * - Attributes: name, label, min, max, sublabel, value, size, maxlength, event * hidden An invisible form field. * - Attributes: name, value * header A level 5 header. * - Attributes: label * div A generic placeholder element or label. * - Attributes: name, label * submit A submit button. Morebits.simpleWindow moves these to the footer of the dialog. * - Attributes: name, label, disabled * button A generic button. * - Attributes: name, label, disabled, event * textarea A big, multi-line text box. * - Attributes: name, label, value, cols, rows, disabled, readonly * * Global attributes: id, style, tooltip, extra, adminonly */ Morebits.quickForm = function QuickForm( event, eventType ) { this.root = new Morebits.quickForm.element( { type: 'form', event: event, eventType:eventType } ); }; Morebits.quickForm.prototype.render = function QuickFormRender() { var ret = this.root.render(); ret.names = {}; return ret; }; Morebits.quickForm.prototype.append = function QuickFormAppend( data ) { return this.root.append( data ); }; Morebits.quickForm.element = function QuickFormElement( data ) { this.data = data; this.childs = []; this.id = Morebits.quickForm.element.id++; }; Morebits.quickForm.element.id = 0; Morebits.quickForm.element.prototype.append = function QuickFormElementAppend( data ) { var child; if( data instanceof Morebits.quickForm.element ) { child = data; } else { child = new Morebits.quickForm.element( data ); } this.childs.push( child ); return child; }; // This should be called without parameters: form.render() Morebits.quickForm.element.prototype.render = function QuickFormElementRender( internal_subgroup_id ) { var currentNode = this.compute( this.data, internal_subgroup_id ); for( var i = 0; i < this.childs.length; ++i ) { // do not pass internal_subgroup_id to recursive calls currentNode[1].appendChild( this.childs[i].render() ); } return currentNode[0]; }; Morebits.quickForm.element.prototype.compute = function QuickFormElementCompute( data, in_id ) { var node; var childContainder = null; var label; var id = ( in_id ? in_id + '_' : '' ) + 'node_' + this.id; if( data.adminonly && !Morebits.userIsInGroup( 'sysop' ) ) { // hell hack alpha data.type = 'hidden'; } var i, current, subnode; switch( data.type ) { case 'form': node = document.createElement( 'form' ); node.className = "quickform"; node.setAttribute( 'action', 'javascript:void(0);'); if( data.event ) { node.addEventListener( data.eventType || 'submit', data.event , false ); } break; case 'select': node = document.createElement( 'div' ); node.setAttribute( 'id', 'div_' + id ); if( data.label ) { label = node.appendChild( document.createElement( 'label' ) ); label.setAttribute( 'for', id ); label.appendChild( document.createTextNode( data.label ) ); } var select = node.appendChild( document.createElement( 'select' ) ); if( data.event ) { select.addEventListener( 'change', data.event, false ); } if( data.multiple ) { select.setAttribute( 'multiple', 'multiple' ); } if( data.size ) { select.setAttribute( 'size', data.size ); } select.setAttribute( 'name', data.name ); if( data.list ) { for( i = 0; i < data.list.length; ++i ) { current = data.list[i]; if( current.list ) { current.type = 'optgroup'; } else { current.type = 'option'; } subnode = this.compute( current ); select.appendChild( subnode[0] ); } } childContainder = select; break; case 'option': node = document.createElement( 'option' ); node.values = data.value; node.setAttribute( 'value', data.value ); if( data.selected ) { node.setAttribute( 'selected', 'selected' ); } if( data.disabled ) { node.setAttribute( 'disabled', 'disabled' ); } node.setAttribute( 'label', data.label ); node.appendChild( document.createTextNode( data.label ) ); break; case 'optgroup': node = document.createElement( 'optgroup' ); node.setAttribute( 'label', data.label ); if( data.list ) { for( i = 0; i < data.list.length; ++i ) { current = data.list[i]; current.type = 'option'; //must be options here subnode = this.compute( current ); node.appendChild( subnode[0] ); } } break; case 'field': node = document.createElement( 'fieldset' ); label = node.appendChild( document.createElement( 'legend' ) ); label.appendChild( document.createTextNode( data.label ) ); if( data.name ) { node.setAttribute( 'name', data.name ); } break; case 'checkbox': case 'radio': node = document.createElement( 'div' ); if( data.list ) { for( i = 0; i < data.list.length; ++i ) { var cur_id = id + '_' + i; current = data.list[i]; var cur_div; if( current.type === 'header' ) { // inline hack cur_div = node.appendChild( document.createElement( 'h6' ) ); cur_div.appendChild( document.createTextNode( current.label ) ); if( current.tooltip ) { Morebits.quickForm.element.generateTooltip( cur_div , current ); } continue; } cur_div = node.appendChild( document.createElement( 'div' ) ); subnode = cur_div.appendChild( document.createElement( 'input' ) ); subnode.values = current.value; subnode.setAttribute( 'value', current.value ); subnode.setAttribute( 'name', current.name || data.name ); subnode.setAttribute( 'type', data.type ); subnode.setAttribute( 'id', cur_id ); if( current.checked ) { subnode.setAttribute( 'checked', 'checked' ); } if( current.disabled ) { subnode.setAttribute( 'disabled', 'disabled' ); } if( data.event ) { subnode.addEventListener( 'change', data.event, false ); } else if ( current.event ) { subnode.addEventListener( 'change', current.event, true ); } label = cur_div.appendChild( document.createElement( 'label' ) ); label.appendChild( document.createTextNode( current.label ) ); label.setAttribute( 'for', cur_id ); if( current.tooltip ) { Morebits.quickForm.element.generateTooltip( label, current ); } var event; if( current.subgroup ) { var tmpgroup = current.subgroup; // $.extend({}, current.subgroup); really needed? if( ! $.isArray( tmpgroup ) ) { tmpgroup = [ tmpgroup ]; } var subgroupRaw = new Morebits.quickForm.element({ type: 'div', id: id + '_' + i + '_subgroup' }); $.each( tmpgroup, function( idx, el ) { if( ! el.type ) { el.type = data.type; } el.name = (current.name || data.name) + '.' + el.name; subgroupRaw.append( el ); } ); var subgroup = subgroupRaw.render( cur_id ); subgroup.className = "quickformSubgroup"; subnode.subgroup = subgroup; subnode.shown = false; event = function(e) { if( e.target.checked ) { e.target.parentNode.appendChild( e.target.subgroup ); if( e.target.type === 'radio' ) { var name = e.target.name; if( e.target.form.names[name] !== undefined ) { e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup ); } e.target.form.names[name] = e.target; } } else { e.target.parentNode.removeChild( e.target.subgroup ); } }; subnode.addEventListener( 'change', event, true ); if( current.checked ) { subnode.parentNode.appendChild( subgroup ); } } else if( data.type === 'radio' ) { event = function(e) { if( e.target.checked ) { var name = e.target.name; if( e.target.form.names[name] !== undefined ) { e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup ); } delete e.target.form.names[name]; } }; subnode.addEventListener( 'change', event, true ); } } } break; case 'input': node = document.createElement( 'div' ); node.setAttribute( 'id', 'div_' + id ); if( data.label ) { label = node.appendChild( document.createElement( 'label' ) ); label.appendChild( document.createTextNode( data.label ) ); label.setAttribute( 'for', id ); } subnode = node.appendChild( document.createElement( 'input' ) ); if( data.value ) { subnode.setAttribute( 'value', data.value ); } subnode.setAttribute( 'name', data.name ); subnode.setAttribute( 'id', id ); subnode.setAttribute( 'type', 'text' ); if( data.size ) { subnode.setAttribute( 'size', data.size ); } if( data.disabled ) { subnode.setAttribute( 'disabled', 'disabled' ); } if( data.readonly ) { subnode.setAttribute( 'readonly', 'readonly' ); } if( data.maxlength ) { subnode.setAttribute( 'maxlength', data.maxlength ); } if( data.event ) { subnode.addEventListener( 'keyup', data.event, false ); } break; case 'dyninput': var min = data.min || 1; var max = data.max || Infinity; node = document.createElement( 'div' ); label = node.appendChild( document.createElement( 'h5' ) ); label.appendChild( document.createTextNode( data.label ) ); var listNode = node.appendChild( document.createElement( 'div' ) ); var more = this.compute( { type: 'button', label: 'more', disabled: min >= max, event: function(e) { var area = e.target.area; var new_node = new Morebits.quickForm.element( e.target.sublist ); e.target.area.appendChild( new_node.render() ); if( ++e.target.counter >= e.target.max ) { e.target.setAttribute( 'disabled', 'disabled' ); } e.stopPropagation(); } } ); node.appendChild( more[0] ); var moreButton = more[1]; var sublist = { type: '_dyninput_element', label: data.sublabel || data.label, name: data.name, value: data.value, size: data.size, remove: false, maxlength: data.maxlength, event: data.event }; for( i = 0; i < min; ++i ) { var elem = new Morebits.quickForm.element( sublist ); listNode.appendChild( elem.render() ); } sublist.remove = true; sublist.morebutton = moreButton; sublist.listnode = listNode; moreButton.sublist = sublist; moreButton.area = listNode; moreButton.max = max - min; moreButton.counter = 0; break; case '_dyninput_element': // Private, similar to normal input node = document.createElement( 'div' ); if( data.label ) { label = node.appendChild( document.createElement( 'label' ) ); label.appendChild( document.createTextNode( data.label ) ); label.setAttribute( 'for', id ); } subnode = node.appendChild( document.createElement( 'input' ) ); if( data.value ) { subnode.setAttribute( 'value', data.value ); } subnode.setAttribute( 'name', data.name ); subnode.setAttribute( 'type', 'text' ); if( data.size ) { subnode.setAttribute( 'size', data.size ); } if( data.maxlength ) { subnode.setAttribute( 'maxlength', data.maxlength ); } if( data.event ) { subnode.addEventListener( 'keyup', data.event, false ); } if( data.remove ) { var remove = this.compute( { type: 'button', label: 'remove', event: function(e) { var list = e.target.listnode; var node = e.target.inputnode; var more = e.target.morebutton; list.removeChild( node ); --more.counter; more.removeAttribute( 'disabled' ); e.stopPropagation(); } } ); node.appendChild( remove[0] ); var removeButton = remove[1]; removeButton.inputnode = node; removeButton.listnode = data.listnode; removeButton.morebutton = data.morebutton; } break; case 'hidden': node = document.createElement( 'input' ); node.setAttribute( 'type', 'hidden' ); node.values = data.value; node.setAttribute( 'value', data.value ); node.setAttribute( 'name', data.name ); break; case 'header': node = document.createElement( 'h5' ); node.appendChild( document.createTextNode( data.label ) ); break; case 'div': node = document.createElement( 'div' ); if (data.name) { node.setAttribute( 'name', data.name ); } if (data.label) { if ( ! $.isArray( data.label ) ) { data.label = [ data.label ]; } var result = document.createElement( 'span' ); result.className = 'quickformDescription'; for( i = 0; i < data.label.length; ++i ) { if( typeof data.label[i] === 'string' ) { result.appendChild( document.createTextNode( data.label[i] ) ); } else if( data.label[i] instanceof Element ) { result.appendChild( data.label[i] ); } } node.appendChild( result ); } break; case 'submit': node = document.createElement( 'span' ); childContainder = node.appendChild(document.createElement( 'input' )); childContainder.setAttribute( 'type', 'submit' ); if( data.label ) { childContainder.setAttribute( 'value', data.label ); } childContainder.setAttribute( 'name', data.name || 'submit' ); if( data.disabled ) { childContainder.setAttribute( 'disabled', 'disabled' ); } break; case 'button': node = document.createElement( 'span' ); childContainder = node.appendChild(document.createElement( 'input' )); childContainder.setAttribute( 'type', 'button' ); if( data.label ) { childContainder.setAttribute( 'value', data.label ); } childContainder.setAttribute( 'name', data.name ); if( data.disabled ) { childContainder.setAttribute( 'disabled', 'disabled' ); } if( data.event ) { childContainder.addEventListener( 'click', data.event, false ); } break; case 'textarea': node = document.createElement( 'div' ); node.setAttribute( 'id', 'div_' + id ); if( data.label ) { label = node.appendChild( document.createElement( 'h5' ) ); label.appendChild( document.createTextNode( data.label ) ); // TODO need to nest a <label> tag in here without creating extra vertical space //label.setAttribute( 'for', id ); } subnode = node.appendChild( document.createElement( 'textarea' ) ); subnode.setAttribute( 'name', data.name ); if( data.cols ) { subnode.setAttribute( 'cols', data.cols ); } if( data.rows ) { subnode.setAttribute( 'rows', data.rows ); } if( data.disabled ) { subnode.setAttribute( 'disabled', 'disabled' ); } if( data.readonly ) { subnode.setAttribute( 'readonly', 'readonly' ); } if( data.value ) { subnode.value = data.value; } break; default: throw new Error("Morebits.quickForm: unknown element type " + data.type.toString()); } if( !childContainder ) { childContainder = node; } if( data.tooltip ) { Morebits.quickForm.element.generateTooltip( label || node , data ); } if( data.extra ) { childContainder.extra = data.extra; } if( data.style ) { childContainder.setAttribute( 'style', data.style ); } childContainder.setAttribute( 'id', data.id || id ); return [ node, childContainder ]; }; Morebits.quickForm.element.generateTooltip = function QuickFormElementGenerateTooltip( node, data ) { $('<span/>', { 'class': 'ui-icon ui-icon-help ui-icon-inline morebits-tooltip' }).appendTo(node).tipsy({ 'fallback': data.tooltip, 'fade': true, 'gravity': $.fn.tipsy.autoWE, 'html': true, 'delayOut': 250 }); }; /** * Some utility methods for manipulating quickForms after their creation * (None of them work for "dyninput" type fields at present) * * Morebits.quickForm.getElements(form, fieldName) * Returns all form elements with a given field name or ID * * Morebits.quickForm.getCheckboxOrRadio(elementArray, value) * Searches the array of elements for a checkbox or radio button with a certain |value| attribute * * Morebits.quickForm.getElementContainer(element) * Returns the <div> containing the form element, or the form element itself * May not work as expected on checkboxes or radios * * Morebits.quickForm.getElementLabelObject(element) * Gets the HTML element that contains the label of the given form element (mainly for internal use) * * Morebits.quickForm.getElementLabel(element) * Gets the label text of the element * * Morebits.quickForm.setElementLabel(element, labelText) * Sets the label of the element to the given text * * Morebits.quickForm.overrideElementLabel(element, temporaryLabelText) * Stores the element's current label, and temporarily sets the label to the given text * * Morebits.quickForm.resetElementLabel(element) * Restores the label stored by overrideElementLabel * * Morebits.quickForm.setElementVisibility(element, visibility) * Shows or hides a form element plus its label and tooltip * * Morebits.quickForm.setElementTooltipVisibility(element, visibility) * Shows or hides the "question mark" icon next to a form element */ Morebits.quickForm.getElements = function QuickFormGetElements(form, fieldName) { var $form = $(form); var $elements = $form.find('[name="' + fieldName + '"]'); if ($elements.length > 0) { return $elements.toArray(); } $elements = $form.find('#' + fieldName); if ($elements.length > 0) { return $elements.toArray(); } return null; }; Morebits.quickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) { var found = $.grep(elementArray, function(el) { return el.value === value; }); if (found.length > 0) { return found[0]; } return null; }; Morebits.quickForm.getElementContainer = function QuickFormGetElementContainer(element) { // for divs, headings and fieldsets, the container is the element itself if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) { return element; } // for others, just return the parent node return element.parentNode; }; Morebits.quickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) { // for buttons, divs and headers, the label is on the element itself if (element.type === "button" || element.type === "submit" || element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) { return element; // for fieldsets, the label is the child <legend> element } else if (element instanceof HTMLFieldSetElement) { return element.getElementsByTagName("legend")[0]; // for textareas, the label is the sibling <h5> element } else if (element instanceof HTMLTextAreaElement) { return element.parentNode.getElementsByTagName("h5")[0]; // for others, the label is the sibling <label> element } else { return element.parentNode.getElementsByTagName("label")[0]; } return null; }; Morebits.quickForm.getElementLabel = function QuickFormGetElementLabel(element) { var labelElement = Morebits.quickForm.getElementLabelObject(element); if (!labelElement) { return null; } return labelElement.firstChild.textContent; }; Morebits.quickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) { var labelElement = Morebits.quickForm.getElementLabelObject(element); if (!labelElement) { return false; } labelElement.firstChild.textContent = labelText; return true; }; Morebits.quickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) { if (!element.hasAttribute("data-oldlabel")) { element.setAttribute("data-oldlabel", Morebits.quickForm.getElementLabel(element)); } return Morebits.quickForm.setElementLabel(element, temporaryLabelText); }; Morebits.quickForm.resetElementLabel = function QuickFormResetElementLabel(element) { if (element.hasAttribute("data-oldlabel")) { return Morebits.quickForm.setElementLabel(element, element.getAttribute("data-oldlabel")); } return null; }; Morebits.quickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) { $(element).toggle(visibility); }; Morebits.quickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) { $(Morebits.quickForm.getElementContainer(element)).find(".morebits-tooltip").toggle(visibility); }; /** * **************** HTMLFormElement **************** * * getChecked: * XXX Doesn't seem to work reliably across all browsers at the moment. -- see getChecked2 in twinkleunlink.js, which is better * * Returns an array containing the values of elements with the given name, that has it's * checked property set to true. (i.e. a checkbox or a radiobutton is checked), or select options * that have selected set to true. (don't try to mix selects with radio/checkboxes, please) * Type is optional and can specify if either radio or checkbox (for the event * that both checkboxes and radiobuttons have the same name. */ HTMLFormElement.prototype.getChecked = function( name, type ) { var elements = this.elements[name]; if( !elements ) { // if the element doesn't exists, return null. return null; } var return_array = []; var i; if( elements instanceof HTMLSelectElement ) { var options = elements.options; for( i = 0; i < options.length; ++i ) { if( options[i].selected ) { if( options[i].values ) { return_array.push( options[i].values ); } else { return_array.push( options[i].value ); } } } } else if( elements instanceof HTMLInputElement ) { if( type && elements.type !== type ) { return []; } else if( elements.checked ) { return [ elements.value ]; } } else { for( i = 0; i < elements.length; ++i ) { if( elements[i].checked ) { if( type && elements[i].type !== type ) { continue; } if( elements[i].values ) { return_array.push( elements[i].values ); } else { return_array.push( elements[i].value ); } } } } return return_array; }; /** * **************** RegExp **************** * * RegExp.escape: Will escape a string to be used in a RegExp */ RegExp.escape = function( text, space_fix ) { text = mw.RegExp.escape(text); // Special MediaWiki escape - underscore/space are often equivalent if( space_fix ) { text = text.replace( / |_/g, '[_ ]' ); } return text; }; /** * **************** Morebits.bytes **************** * Utility object for formatting byte values */ Morebits.bytes = function( value ) { if( typeof value === 'string' ) { var res = /(\d+) ?(\w?)(i?)B?/.exec( value ); var number = res[1]; var mag = res[2]; var si = res[3]; if( !number ) { this.number = 0; return; } if( !si ) { this.value = number * Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 ); } else { this.value = number * Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 ); } } else { this.value = value; } }; Morebits.bytes.magnitudes = { '': 0, 'K': 1, 'M': 2, 'G': 3, 'T': 4, 'P': 5, 'E': 6, 'Z': 7, 'Y': 8 }; Morebits.bytes.rmagnitudes = { 0: '', 1: 'K', 2: 'M', 3: 'G', 4: 'T', 5: 'P', 6: 'E', 7: 'Z', 8: 'Y' }; Morebits.bytes.prototype.valueOf = function() { return this.value; }; Morebits.bytes.prototype.toString = function( magnitude ) { var tmp = this.value; if( magnitude ) { var si = /i/.test(magnitude); var mag = magnitude.replace( /.*?(\w)i?B?.*/g, '$1' ); if( si ) { tmp /= Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 ); } else { tmp /= Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 ); } if( parseInt( tmp, 10 ) !== tmp ) { tmp = Number( tmp ).toPrecision( 4 ); } return tmp + ' ' + mag + (si?'i':'') + 'B'; } else { // si per default var current = 0; while( tmp >= 1024 ) { tmp /= 1024; ++current; } tmp = this.value / Math.pow( 2, current * 10 ); if( parseInt( tmp, 10 ) !== tmp ) { tmp = Number( tmp ).toPrecision( 4 ); } return tmp + ' ' + Morebits.bytes.rmagnitudes[current] + ( current > 0 ? 'iB' : 'B' ); } }; /** * **************** String; Morebits.string **************** */ if (!String.prototype.trimLeft) { String.prototype.trimLeft = function stringPrototypeLtrim( chars ) { chars = chars || "\\s"; return this.replace( new RegExp("^[" + chars + "]+", "g"), "" ); }; } if (!String.prototype.trimRight) { String.prototype.trimRight = function stringPrototypeRtrim( chars ) { chars = chars || "\\s"; return this.replace( new RegExp("[" + chars + "]+$", "g"), "" ); }; } if (!String.prototype.trim) { String.prototype.trim = function stringPrototypeTrim( chars ) { return this.trimRight(chars).trimLeft(chars); }; } // Helper functions to change case of a string Morebits.string = { toUpperCaseFirstChar: function(str) { str = str.toString(); return str.substr( 0, 1 ).toUpperCase() + str.substr( 1 ); }, toLowerCaseFirstChar: function(str) { str = str.toString(); return str.substr( 0, 1 ).toLowerCase() + str.substr( 1 ); }, splitWeightedByKeys: function( str, start, end, skip ) { if( start.length !== end.length ) { throw new Error( 'start marker and end marker must be of the same length' ); } var level = 0; var initial = null; var result = []; if( ! $.isArray( skip ) ) { if( skip === undefined ) { skip = []; } else if( typeof skip === 'string' ) { skip = [ skip ]; } else { throw new Error( "non-applicable skip parameter" ); } } for( var i = 0; i < str.length; ++i ) { for( var j = 0; j < skip.length; ++j ) { if( str.substr( i, skip[j].length ) === skip[j] ) { i += skip[j].length - 1; continue; } } if( str.substr( i, start.length ) === start ) { if( initial === null ) { initial = i; } ++level; i += start.length - 1; } else if( str.substr( i, end.length ) === end ) { --level; i += end.length - 1; } if( !level && initial !== null ) { result.push( str.substring( initial, i + 1 ) ); initial = null; } } return result; } }; /** * **************** Morebits.array **************** * * uniq(arr): returns a copy of the array with duplicates removed * * dups(arr): returns a copy of the array with the first instance of each value * removed; subsequent instances of those values (duplicates) remain * * chunk(arr, size): breaks up |arr| into smaller arrays of length |size|, and * returns an array of these "chunked" arrays */ Morebits.array = { uniq: function(arr) { if ( ! $.isArray( arr ) ) { throw "A non-array object passed to Morebits.array.uniq"; } var result = []; for( var i = 0; i < arr.length; ++i ) { var current = arr[i]; if( result.indexOf( current ) === -1 ) { result.push( current ); } } return result; }, dups: function(arr) { if ( ! $.isArray( arr ) ) { throw "A non-array object passed to Morebits.array.dups"; } var uniques = []; var result = []; for( var i = 0; i < arr.length; ++i ) { var current = arr[i]; if( uniques.indexOf( current ) === -1 ) { uniques.push( current ); } else { result.push( current ); } } return result; }, chunk: function( arr, size ) { if ( ! $.isArray( arr ) ) { throw "A non-array object passed to Morebits.array.chunk"; } if( typeof size !== 'number' || size <= 0 ) { // pretty impossible to do anything :) return [ arr ]; // we return an array consisting of this array. } var result = []; var current; for( var i = 0; i < arr.length; ++i ) { if( i % size === 0 ) { // when 'i' is 0, this is always true, so we start by creating one. current = []; result.push( current ); } current.push( arr[i] ); } return result; } }; /** * **************** Morebits.getPageAssociatedUser **************** * Get the user associated with the currently-viewed page. * Currently works on User:, User talk:, Special:Contributions. */ Morebits.getPageAssociatedUser = function(){ var thisNamespaceId = mw.config.get('wgNamespaceNumber'); if ( thisNamespaceId === 2 /* User: */ || thisNamespaceId === 3 /* User talk: */ ) { return mw.config.get('wgTitle').split( '/' )[0]; // only first part before any slashes, to work on subpages } if ( thisNamespaceId === -1 /* Special: */ && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) { return mw.config.get("wgRelevantUserName"); } return false; }; /** * **************** Morebits.unbinder **************** * Used by Morebits.wikitext.page.commentOutImage */ Morebits.unbinder = function Unbinder( string ) { if( typeof string !== 'string' ) { throw new Error( "not a string" ); } this.content = string; this.counter = 0; this.history = {}; this.prefix = '%UNIQ::' + Math.random() + '::'; this.postfix = '::UNIQ%'; } Morebits.unbinder.prototype = { unbind: function UnbinderUnbind( prefix, postfix ) { var re = new RegExp( prefix + '(.*?)' + postfix, 'g' ); this.content = this.content.replace( re, Morebits.unbinder.getCallback( this ) ); }, rebind: function UnbinderRebind() { var content = this.content; content.self = this; for( var current in this.history ) { if( this.history.hasOwnProperty( current ) ) { content = content.replace( current, this.history[current] ); } } return content; }, prefix: null, // %UNIQ::0.5955981644938324:: postfix: null, // ::UNIQ% content: null, // string counter: null, // 0++ history: null // {} }; Morebits.unbinder.getCallback = function UnbinderGetCallback(self) { return function UnbinderCallback( match , a , b ) { var current = self.prefix + self.counter + self.postfix; self.history[current] = match; ++self.counter; return current; }; }; /** * **************** Date **************** * Helper functions to get the month as a string instead of a number * * Normally it is poor form to play with prototypes of primitive types, but it * is fairly unlikely that anyone will iterate over a Date object. */ Date.monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; Date.monthNamesAbbrev = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; Date.prototype.getMonthName = function() { return Date.monthNames[ this.getMonth() ]; }; Date.prototype.getMonthNameAbbrev = function() { return Date.monthNamesAbbrev[ this.getMonth() ]; }; Date.prototype.getUTCMonthName = function() { return Date.monthNames[ this.getUTCMonth() ]; }; Date.prototype.getUTCMonthNameAbbrev = function() { return Date.monthNamesAbbrev[ this.getUTCMonth() ]; }; /** * **************** Morebits.wikipedia **************** * English Wikipedia-specific objects */ Morebits.wikipedia = {}; Morebits.wikipedia.namespaces = { '-2': 'Media', '-1': 'Special', '0': '', '1': 'Talk', '2': 'User', '3': 'User talk', '4': 'Project', '5': 'Project talk', '6': 'File', '7': 'File talk', '8': 'MediaWiki', '9': 'MediaWiki talk', '10': 'Template', '11': 'Template talk', '12': 'Help', '13': 'Help talk', '14': 'Category', '15': 'Category talk', '100': 'Portal', '101': 'Portal talk', '108': 'Book', '109': 'Book talk' }; Morebits.wikipedia.namespacesFriendly = { '0': '(Article)', '1': 'Talk', '2': 'User', '3': 'User talk', '4': 'Wikipedia', '5': 'Wikipedia talk', '6': 'File', '7': 'File talk', '8': 'MediaWiki', '9': 'MediaWiki talk', '10': 'Template', '11': 'Template talk', '12': 'Help', '13': 'Help talk', '14': 'Category', '15': 'Category talk', '100': 'Portal', '101': 'Portal talk', '108': 'Book', '109': 'Book talk' }; /** * **************** Morebits.wiki **************** * Various objects for wiki editing and API access */ Morebits.wiki = {}; // Analyzes the HTML of the current page (i.e. no AJAX requests) to determine if it // is a redirect or soft redirect Morebits.wiki.isPageRedirect = function wikipediaIsPageRedirect() { return !!($("span.redirectText").length > 0 || document.getElementById("softredirect")); }; /** * **************** Morebits.wiki.actionCompleted **************** * * Use of Morebits.wiki.actionCompleted(): * Every call to Morebits.wiki.api.post() results in the dispatch of * an asynchronous callback. Each callback can in turn * make an additional call to Morebits.wiki.api.post() to continue a * processing sequence. At the conclusion of the final callback * of a processing sequence, it is not possible to simply return to the * original caller because there is no call stack leading back to * the original context. Instead, Morebits.wiki.actionCompleted.event() is * called to display the result to the user and to perform an optional * page redirect. * * The determination of when to call Morebits.wiki.actionCompleted.event() * is managed through the globals Morebits.wiki.numberOfActionsLeft and * Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is * incremented at the start of every Morebits.wiki.api call and decremented * after the completion of a callback function. If a callback function * does not create a new Morebits.wiki.api object before exiting, it is the * final step in the processing chain and Morebits.wiki.actionCompleted.event() * will then be called. * * Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that * processing is not complete upon the conclusion of the final callback function. * This is used for batch operations. The end of a batch is signaled by calling * Morebits.wiki.removeCheckpoint(). */ Morebits.wiki.numberOfActionsLeft = 0; Morebits.wiki.nbrOfCheckpointsLeft = 0; Morebits.wiki.actionCompleted = function( self ) { if( --Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0 ) { Morebits.wiki.actionCompleted.event( self ); } }; // Change per action wanted Morebits.wiki.actionCompleted.event = function() { new Morebits.status( Morebits.wiki.actionCompleted.notice, Morebits.wiki.actionCompleted.postfix, 'info' ); if( Morebits.wiki.actionCompleted.redirect ) { // if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones. if( !( (/^\w+\:\/\//).test( Morebits.wiki.actionCompleted.redirect ) ) ) { Morebits.wiki.actionCompleted.redirect = mw.util.getUrl( Morebits.wiki.actionCompleted.redirect ); if( Morebits.wiki.actionCompleted.followRedirect === false ) { Morebits.wiki.actionCompleted.redirect += "?redirect=no"; } } window.setTimeout( function() { window.location = Morebits.wiki.actionCompleted.redirect; }, Morebits.wiki.actionCompleted.timeOut ); } }; Morebits.wiki.actionCompleted.timeOut = ( typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut ); Morebits.wiki.actionCompleted.redirect = null; Morebits.wiki.actionCompleted.notice = 'Action'; Morebits.wiki.actionCompleted.postfix = 'completed'; Morebits.wiki.addCheckpoint = function() { ++Morebits.wiki.nbrOfCheckpointsLeft; }; Morebits.wiki.removeCheckpoint = function() { if( --Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0 ) { Morebits.wiki.actionCompleted.event(); } }; /** * **************** Morebits.wiki.api **************** * An easy way to talk to the MediaWiki API. * * Constructor parameters: * currentAction: the current action (required) * query: the query (required) * onSuccess: the function to call when request gotten * statusElement: a Morebits.status object to use for status messages (optional) * onError: the function to call if an error occurs (optional) */ Morebits.wiki.api = function( currentAction, query, onSuccess, statusElement, onError ) { this.currentAction = currentAction; this.query = query; this.query.format = 'xml'; this.query.assert = 'user'; this.onSuccess = onSuccess; this.onError = onError; if( statusElement ) { this.statelem = statusElement; this.statelem.status( currentAction ); } else { this.statelem = new Morebits.status( currentAction ); } }; Morebits.wiki.api.prototype = { currentAction: '', onSuccess: null, onError: null, parent: window, // use global context if there is no parent object query: null, responseXML: null, setParent: function(parent) { this.parent = parent; }, // keep track of parent object for callbacks statelem: null, // this non-standard name kept for backwards compatibility statusText: null, // result received from the API, normally "success" or "error" errorCode: null, // short text error code, if any, as documented in the MediaWiki API errorText: null, // full error description, if any // post(): carries out the request // do not specify a parameter unless you really really want to give jQuery some extra parameters post: function( callerAjaxParameters ) { ++Morebits.wiki.numberOfActionsLeft; var ajaxparams = $.extend( {}, { context: this, type: 'POST', url: mw.util.wikiScript('api'), data: Morebits.queryString.create(this.query), datatype: 'xml', headers: { 'Api-User-Agent': morebitsWikiApiUserAgent }, success: function(xml, statusText, jqXHR) { this.statusText = statusText; this.responseXML = xml; this.errorCode = $(xml).find('error').attr('code'); this.errorText = $(xml).find('error').attr('info'); if (typeof this.errorCode === "string") { // the API didn't like what we told it, e.g., bad edit token or an error creating a page this.returnError(); return; } // invoke success callback if one was supplied if (this.onSuccess) { // set the callback context to this.parent for new code and supply the API object // as the first argument to the callback (for legacy code) this.onSuccess.call( this.parent, this ); } else { this.statelem.info("done"); } Morebits.wiki.actionCompleted(); }, // only network and server errors reach here – complaints from the API itself are caught in success() error: function(jqXHR, statusText, errorThrown) { this.statusText = statusText; this.errorThrown = errorThrown; // frequently undefined this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.'; this.returnError(); } }, callerAjaxParameters ); return $.ajax( ajaxparams ); // the return value should be ignored, unless using callerAjaxParameters with |async: false| }, returnError: function() { this.statelem.error( this.errorText ); // invoke failure callback if one was supplied if (this.onError) { // set the callback context to this.parent for new code and supply the API object // as the first argument to the callback for legacy code this.onError.call( this.parent, this ); } // don't complete the action so that the error remains displayed }, getStatusElement: function() { return this.statelem; }, getErrorCode: function() { return this.errorCode; }, getErrorText: function() { return this.errorText; }, getXML: function() { return this.responseXML; } }; // Custom user agent header, used by WMF for server-side logging // See https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html var morebitsWikiApiUserAgent = 'morebits.js/2.0 ([[w:WT:TW]])'; // Sets the custom user agent header Morebits.wiki.api.setApiUserAgent = function( ua ) { morebitsWikiApiUserAgent = ( ua ? ua + ' ' : '' ) + 'morebits.js/2.0 ([[w:WT:TW]])'; }; /** * **************** Morebits.wiki.page **************** * Uses the MediaWiki API to load a page and optionally edit it, move it, etc. * * Callers are not permitted to directly access the properties of this class! * All property access is through the appropriate get___() or set___() method. * * Callers should set Morebits.wiki.actionCompleted.notice and Morebits.wiki.actionCompleted.redirect * before the first call to Morebits.wiki.page.load(). * * Each of the callback functions takes one parameter, which is a * reference to the Morebits.wiki.page object that registered the callback. * Callback functions may invoke any Morebits.wiki.page prototype method using this reference. * * * NOTE: This list of member functions is incomplete. * * Constructor: Morebits.wiki.page(pageName, currentAction) * pageName - the name of the page, prefixed by the namespace (if any) * (for the current page, use mw.config.get('wgPageName')) * currentAction - a string describing the action about to be undertaken (optional) * * load(onSuccess, onFailure): Loads the text for the page * onSuccess - callback function which is called when the load has succeeded * onFailure - callback function which is called when the load fails (optional) * XXX onFailure for load() is not yet implemented – do we need it? -- UncleDouggie * probably not -- TTO * * save(onSuccess, onFailure): Saves the text for the page. Must be preceded by calling load(). * onSuccess - callback function which is called when the save has succeeded (optional) * onFailure - callback function which is called when the save fails (optional) * Warning: Calling save() can result in additional calls to the previous load() callbacks to * recover from edit conflicts! * In this case, callers must make the same edit to the new pageText and reinvoke save(). * This behavior can be disabled with setMaxConflictRetries(0). * * append(onSuccess, onFailure): Adds the text provided via setAppendText() to the end of the page. * Does not require calling load() first. * onSuccess - callback function which is called when the method has succeeded (optional) * onFailure - callback function which is called when the method fails (optional) * * prepend(onSuccess, onFailure): Adds the text provided via setPrependText() to the start of the page. * Does not require calling load() first. * onSuccess - callback function which is called when the method has succeeded (optional) * onFailure - callback function which is called when the method fails (optional) * * getPageName(): returns a string containing the name of the loaded page, including the namespace * * getPageText(): returns a string containing the text of the page after a successful load() * * setPageText(pageText) * pageText - string containing the updated page text that will be saved when save() is called * * setAppendText(appendText) * appendText - string containing the text that will be appended to the page when append() is called * * setPrependText(prependText) * prependText - string containing the text that will be prepended to the page when prepend() is called * * setEditSummary(summary) * summary - string containing the text of the edit summary that will be used when save() is called * * setMinorEdit(minorEdit) * minorEdit is a boolean value: * true - When save is called, the resulting edit will be marked as "minor". * false - When save is called, the resulting edit will not be marked as "minor". (default) * * setPageSection(pageSection) * pageSection - integer specifying the section number to load or save. The default is |null|, which means * that the entire page will be retrieved. * * setMaxConflictRetries(maxRetries) * maxRetries - number of retries for save errors involving an edit conflict or loss of edit token * default: 2 * * setMaxRetries(maxRetries) * maxRetries - number of retries for save errors not involving an edit conflict or loss of edit token * default: 2 * * setCallbackParameters(callbackParameters) * callbackParameters - an object for use in a callback function * * getCallbackParameters(): returns the object previous set by setCallbackParameters() * * Callback notes: callbackParameters is for use by the caller only. The parameters * allow a caller to pass the proper context into its callback function. * Callers must ensure that any changes to the callbackParameters object * within a load() callback still permit a proper re-entry into the * load() callback if an edit conflict is detected upon calling save(). * * getStatusElement(): returns the Status element created by the constructor * * setFollowRedirect(followRedirect) * followRedirect is a boolean value: * true - a maximum of one redirect will be followed. * In the event of a redirect, a message is displayed to the user and * the redirect target can be retrieved with getPageName(). * false - the requested pageName will be used without regard to any redirect. (default) * * setWatchlist(watchlistOption) * watchlistOption is a boolean value: * true - page will be added to the user's watchlist when save() is called * false - watchlist status of the page will not be changed (default) * * setWatchlistFromPreferences(watchlistOption) * watchlistOption is a boolean value: * true - page watchlist status will be set based on the user's * preference settings when save() is called * false - watchlist status of the page will not be changed (default) * * Watchlist notes: * 1. The MediaWiki API value of 'unwatch', which explicitly removes the page from the * user's watchlist, is not used. * 2. If both setWatchlist() and setWatchlistFromPreferences() are called, * the last call takes priority. * 3. Twinkle modules should use the appropriate preference to set the watchlist options. * 4. Most Twinkle modules use setWatchlist(). * setWatchlistFromPreferences() is only needed for the few Twinkle watchlist preferences * that accept a string value of 'default'. * * setCreateOption(createOption) * createOption is a string value: * 'recreate' - create the page if it does not exist, or edit it if it exists * 'createonly' - create the page if it does not exist, but return an error if it * already exists * 'nocreate' - don't create the page, only edit it if it already exists * null - create the page if it does not exist, unless it was deleted in the moment * between retrieving the edit token and saving the edit (default) * * exists(): returns true if the page existed on the wiki when it was last loaded * * lookupCreator(onSuccess): Retrieves the username of the user who created the page * onSuccess - callback function which is called when the username is found * within the callback, the username can be retrieved using the getCreator() function * * getCreator(): returns the user who created the page following lookupCreator() * * patrol(): marks the page as patrolled (only when "rcid" is present in the query string) * * move(onSuccess, onFailure): Moves a page to another title * * deletePage(onSuccess, onFailure): Deletes a page (for admins only) * */ /** * Call sequence for common operations (optional final user callbacks not shown): * * Edit current contents of a page (no edit conflict): * .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() * * Edit current contents of a page (with edit conflict): * .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveError() -> * ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() * * Append to a page (similar for prepend): * .append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() * * Notes: * 1. All functions following Morebits.wiki.api.post() are invoked asynchronously * from the jQuery AJAX library. * 2. The sequence for append/prepend could be slightly shortened, but it would require * significant duplication of code for little benefit. */ Morebits.wiki.page = function(pageName, currentAction) { if (!currentAction) { currentAction = 'Opening page "' + pageName + '"'; } /** * Private context variables * * This context is not visible to the outside, thus all the data here * must be accessed via getter and setter functions. */ var ctx = { // backing fields for public properties pageName: pageName, pageExists: false, editSummary: null, callbackParameters: null, statusElement: new Morebits.status(currentAction), // - edit pageText: null, editMode: 'all', // save() replaces entire contents of the page by default appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect createOption: null, minorEdit: false, pageSection: null, maxConflictRetries: 2, maxRetries: 2, followRedirect: false, watchlistOption: 'nochange', creator: null, // - revert revertOldID: null, // - move moveDestination: null, moveTalkPage: false, moveSubpages: false, moveSuppressRedirect: false, // - protect protectEdit: null, protectMove: null, protectCreate: null, protectCascade: false, // - stabilize (FlaggedRevs) flaggedRevs: null, // internal status pageLoaded: false, editToken: null, loadTime: null, lastEditTime: null, revertCurID: null, revertUser: null, fullyProtected: false, conflictRetries: 0, retries: 0, // callbacks onLoadSuccess: null, onLoadFailure: null, onSaveSuccess: null, onSaveFailure: null, onLookupCreatorSuccess: null, onMoveSuccess: null, onMoveFailure: null, onDeleteSuccess: null, onDeleteFailure: null, onProtectSuccess: null, onProtectFailure: null, onStabilizeSuccess: null, onStabilizeFailure: null, // internal objects loadQuery: null, loadApi: null, saveApi: null, lookupCreatorApi: null, moveApi: null, moveProcessApi: null, deleteApi: null, deleteProcessApi: null, protectApi: null, protectProcessApi: null, stabilizeApi: null, stabilizeProcessApi: null }; var emptyFunction = function() { }; /** * Public interface accessors */ this.getPageName = function() { return ctx.pageName; }; this.getPageText = function() { return ctx.pageText; }; this.setPageText = function(pageText) { ctx.editMode = 'all'; ctx.pageText = pageText; }; this.setAppendText = function(appendText) { ctx.editMode = 'append'; ctx.appendText = appendText; }; this.setPrependText = function(prependText) { ctx.editMode = 'prepend'; ctx.prependText = prependText; }; this.setEditSummary = function(summary) { ctx.editSummary = summary; }; this.setCreateOption = function(createOption) { ctx.createOption = createOption; }; this.setMinorEdit = function(minorEdit) { ctx.minorEdit = minorEdit; }; this.setPageSection = function(pageSection) { ctx.pageSection = pageSection; }; this.setMaxConflictRetries = function(maxRetries) { ctx.maxConflictRetries = maxRetries; }; this.setMaxRetries = function(maxRetries) { ctx.maxRetries = maxRetries; }; this.setCallbackParameters = function(callbackParameters) { ctx.callbackParameters = callbackParameters; }; this.getCallbackParameters = function() { return ctx.callbackParameters; }; this.getCreator = function() { return ctx.creator; }; this.setOldID = function(oldID) { ctx.revertOldID = oldID; }; this.getRevisionUser = function() { return ctx.revertUser; }; this.setMoveDestination = function(destination) { ctx.moveDestination = destination; }; this.setMoveTalkPage = function(flag) { ctx.moveTalkPage = !!flag; }; this.setMoveSubpages = function(flag) { ctx.moveSubpages = !!flag; }; this.setMoveSuppressRedirect = function(flag) { ctx.moveSuppressRedirect = !!flag; }; this.setEditProtection = function(level, expiry) { ctx.protectEdit = { level: level, expiry: expiry }; }; this.setMoveProtection = function(level, expiry) { ctx.protectMove = { level: level, expiry: expiry }; }; this.setCreateProtection = function(level, expiry) { ctx.protectCreate = { level: level, expiry: expiry }; }; this.setCascadingProtection = function(flag) { ctx.protectCascade = !!flag; }; this.setFlaggedRevs = function(level, expiry) { ctx.flaggedRevs = { level: level, expiry: expiry }; }; this.getStatusElement = function() { return ctx.statusElement; }; this.setFollowRedirect = function(followRedirect) { if (ctx.pageLoaded) { ctx.statusElement.error("Internal error: cannot change redirect setting after the page has been loaded!"); return; } ctx.followRedirect = followRedirect; }; this.setWatchlist = function(flag) { if (flag) { ctx.watchlistOption = 'watch'; } else { ctx.watchlistOption = 'nochange'; } }; this.setWatchlistFromPreferences = function(flag) { if (flag) { ctx.watchlistOption = 'preferences'; } else { ctx.watchlistOption = 'nochange'; } }; this.exists = function() { return ctx.pageExists; }; this.load = function(onSuccess, onFailure) { ctx.onLoadSuccess = onSuccess; ctx.onLoadFailure = onFailure || emptyFunction; // Need to be able to do something after the page loads if (!onSuccess) { ctx.statusElement.error("Internal error: no onSuccess callback provided to load()!"); ctx.onLoadFailure(this); return; } ctx.loadQuery = { action: 'query', prop: 'info|revisions', curtimestamp: '', meta: 'tokens', type: 'csrf', titles: ctx.pageName // don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default }; if (ctx.editMode === 'all') { ctx.loadQuery.rvprop = 'content'; // get the page content at the same time, if needed } else if (ctx.editMode === 'revert') { ctx.loadQuery.rvlimit = 1; ctx.loadQuery.rvstartid = ctx.revertOldID; } if (ctx.followRedirect) { ctx.loadQuery.redirects = ''; // follow all redirects } if (typeof ctx.pageSection === 'number') { ctx.loadQuery.rvsection = ctx.pageSection; } if (Morebits.userIsInGroup('sysop')) { ctx.loadQuery.inprop = 'protection'; } ctx.loadApi = new Morebits.wiki.api("Retrieving page...", ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure); ctx.loadApi.setParent(this); ctx.loadApi.post(); }; // Save updated .pageText to Wikipedia // Only valid after successful .load() this.save = function(onSuccess, onFailure) { ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; if (!ctx.pageLoaded) { ctx.statusElement.error("Internal error: attempt to save a page that has not been loaded!"); ctx.onSaveFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: edit summary not set before save!"); ctx.onSaveFailure(this); return; } if (ctx.fullyProtected && !confirm('You are about to make an edit to the fully protected page "' + ctx.pageName + (ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + ctx.fullyProtected + ')')) + '. \n\nClick OK to proceed with the edit, or Cancel to skip this edit.')) { ctx.statusElement.error("Edit to fully protected page was aborted."); ctx.onSaveFailure(this); return; } ctx.retries = 0; var query = { action: 'edit', title: ctx.pageName, summary: ctx.editSummary, token: ctx.editToken, watchlist: ctx.watchlistOption }; if (typeof ctx.pageSection === 'number') { query.section = ctx.pageSection; } // Set minor edit attribute. If these parameters are present with any value, it is interpreted as true if (ctx.minorEdit) { query.minor = true; } else { query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor" } switch (ctx.editMode) { case 'append': query.appendtext = ctx.appendText; // use mode to append to current page contents break; case 'prepend': query.prependtext = ctx.prependText; // use mode to prepend to current page contents break; case 'revert': query.undo = ctx.revertCurID; query.undoafter = ctx.revertOldID; if (ctx.lastEditTime) { query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded } query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff) break; default: query.text = ctx.pageText; // replace entire contents of the page if (ctx.lastEditTime) { query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded } query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff) break; } if (['recreate', 'createonly', 'nocreate'].indexOf(ctx.createOption) !== -1) { query[ctx.createOption] = ''; } ctx.saveApi = new Morebits.wiki.api( "Saving page...", query, fnSaveSuccess, ctx.statusElement, fnSaveError); ctx.saveApi.setParent(this); ctx.saveApi.post(); }; this.append = function(onSuccess, onFailure) { ctx.editMode = 'append'; ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; this.load(fnAutoSave, ctx.onSaveFailure); }; this.prepend = function(onSuccess, onFailure) { ctx.editMode = 'prepend'; ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; this.load(fnAutoSave, ctx.onSaveFailure); }; this.lookupCreator = function(onSuccess) { if (!onSuccess) { ctx.statusElement.error("Internal error: no onSuccess callback provided to lookupCreator()!"); return; } ctx.onLookupCreatorSuccess = onSuccess; var query = { 'action': 'query', 'prop': 'revisions', 'titles': ctx.pageName, 'rvlimit': 1, 'rvprop': 'user', 'rvdir': 'newer' }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.lookupCreatorApi = new Morebits.wiki.api("Retrieving page creator information", query, fnLookupCreatorSuccess, ctx.statusElement); ctx.lookupCreatorApi.setParent(this); ctx.lookupCreatorApi.post(); }; this.patrol = function() { // look for rcid in querystring; if not, we won't have a patrol token, so no point trying if (!Morebits.queryString.exists("rcid")) { return; } var rcid = Morebits.queryString.get("rcid"); // extract patrol token from "Mark page as patrolled" link on page var patrollinkmatch = /token=(.+)%2B%5C$/.exec($(".patrollink a").attr("href")); if (patrollinkmatch) { var patroltoken = patrollinkmatch[1] + "+\\"; var patrolstat = new Morebits.status("Marking page as patrolled"); var wikipedia_api = new Morebits.wiki.api("doing...", { title: ctx.pageName, action: 'markpatrolled', rcid: rcid, token: patroltoken }, null, patrolstat); wikipedia_api.post({ type: 'GET', url: mw.util.wikiScript('index'), datatype: 'text' // we don't really care about the response }); } }; this.revert = function(onSuccess, onFailure) { ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; if (!ctx.revertOldID) { ctx.statusElement.error("Internal error: revision ID to revert to was not set before revert!"); ctx.onSaveFailure(this); return; } ctx.editMode = 'revert'; this.load(fnAutoSave, ctx.onSaveFailure); }; this.move = function(onSuccess, onFailure) { ctx.onMoveSuccess = onSuccess; ctx.onMoveFailure = onFailure || emptyFunction; if (!ctx.editSummary) { ctx.statusElement.error("Internal error: move reason not set before move (use setEditSummary function)!"); ctx.onMoveFailure(this); return; } if (!ctx.moveDestination) { ctx.statusElement.error("Internal error: destination page name was not set before move!"); ctx.onMoveFailure(this); return; } var query = { action: 'query', prop: 'info', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } if (Morebits.userIsInGroup('sysop')) { query.inprop = 'protection'; } ctx.moveApi = new Morebits.wiki.api("retrieving move token...", query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure); ctx.moveApi.setParent(this); ctx.moveApi.post(); }; // |delete| is a reserved word in some flavours of JS this.deletePage = function(onSuccess, onFailure) { ctx.onDeleteSuccess = onSuccess; ctx.onDeleteFailure = onFailure || emptyFunction; // if a non-admin tries to do this, don't bother if (!Morebits.userIsInGroup('sysop')) { ctx.statusElement.error("Cannot delete page: only admins can do that"); ctx.onDeleteFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: delete reason not set before delete (use setEditSummary function)!"); ctx.onDeleteFailure(this); return; } var query = { action: 'query', prop: 'info', inprop: 'protection', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.deleteApi = new Morebits.wiki.api("retrieving delete token...", query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure); ctx.deleteApi.setParent(this); ctx.deleteApi.post(); }; this.protect = function(onSuccess, onFailure) { ctx.onProtectSuccess = onSuccess; ctx.onProtectFailure = onFailure || emptyFunction; // if a non-admin tries to do this, don't bother if (!Morebits.userIsInGroup('sysop')) { ctx.statusElement.error("Cannot protect page: only admins can do that"); ctx.onProtectFailure(this); return; } if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) { ctx.statusElement.error("Internal error: you must set edit and/or move and/or create protection before calling protect()!"); ctx.onProtectFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: protection reason not set before protect (use setEditSummary function)!"); ctx.onProtectFailure(this); return; } var query = { action: 'query', prop: 'info', inprop: 'protection', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.protectApi = new Morebits.wiki.api("retrieving protect token...", query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure); ctx.protectApi.setParent(this); ctx.protectApi.post(); }; // apply FlaggedRevs protection-style settings // only works where $wgFlaggedRevsProtection = true (i.e. where FlaggedRevs // settings appear on the wiki's "protect" tab) this.stabilize = function(onSuccess, onFailure) { ctx.onStabilizeSuccess = onSuccess; ctx.onStabilizeFailure = onFailure || emptyFunction; // if a non-admin tries to do this, don't bother if (!Morebits.userIsInGroup('sysop')) { ctx.statusElement.error("Cannot apply FlaggedRevs settings: only admins can do that"); ctx.onStabilizeFailure(this); return; } if (!ctx.flaggedRevs) { ctx.statusElement.error("Internal error: you must set flaggedRevs before calling stabilize()!"); ctx.onStabilizeFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: reason not set before calling stabilize() (use setEditSummary function)!"); ctx.onStabilizeFailure(this); return; } var query = { action: 'query', prop: 'info|flagged', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.stabilizeApi = new Morebits.wiki.api("retrieving stabilize token...", query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure); ctx.stabilizeApi.setParent(this); ctx.stabilizeApi.post(); }; /** * Private member functions * * These are not exposed outside */ // callback from loadSuccess() for append() and prepend() threads var fnAutoSave = function(pageobj) { pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure); }; // callback from loadApi.post() var fnLoadSuccess = function() { var xml = ctx.loadApi.getXML(); if ( !fnCheckPageName(xml, ctx.onLoadFailure) ) { return; // abort } ctx.pageExists = ($(xml).find('page').attr('missing') !== ""); if (ctx.pageExists) { ctx.pageText = $(xml).find('rev').text(); } else { ctx.pageText = ''; // allow for concatenation, etc. } // extract protection info, to alert admins when they are about to edit a protected page if (Morebits.userIsInGroup('sysop')) { var editprot = $(xml).find('pr[type="edit"]'); if (editprot.length > 0 && editprot.attr('level') === 'sysop') { ctx.fullyProtected = editprot.attr('expiry'); } else { ctx.fullyProtected = false; } } ctx.editToken = $(xml).find('tokens').attr('csrftoken'); if (!ctx.editToken) { ctx.statusElement.error("Failed to retrieve edit token."); ctx.onLoadFailure(this); return; } ctx.loadTime = $(xml).find('api').attr('curtimestamp'); if (!ctx.loadTime) { ctx.statusElement.error("Failed to retrieve start timestamp."); ctx.onLoadFailure(this); return; } ctx.lastEditTime = $(xml).find('page').attr('touched'); if (ctx.editMode === 'revert') { ctx.revertCurID = $(xml).find('rev').attr('revid'); if (!ctx.revertCurID) { ctx.statusElement.error("Failed to retrieve current revision ID."); ctx.onLoadFailure(this); return; } ctx.revertUser = $(xml).find('rev').attr('user'); if (!ctx.revertUser) { if ($(xml).find('rev').attr('userhidden') === "") { // username was RevDel'd or oversighted ctx.revertUser = "<username hidden>"; } else { ctx.statusElement.error("Failed to retrieve user who made the revision."); ctx.onLoadFailure(this); return; } } // set revert edit summary ctx.editSummary = "[[Help:Revert|Reverted]] to revision " + ctx.revertOldID + " by " + ctx.revertUser + ": " + ctx.editSummary; } ctx.pageLoaded = true; // alert("Generate edit conflict now"); // for testing edit conflict recovery logic ctx.onLoadSuccess(this); // invoke callback }; // helper function to parse the page name returned from the API var fnCheckPageName = function(xml, onFailure) { if (!onFailure) { onFailure = emptyFunction; } // check for invalid titles if ( $(xml).find('page').attr('invalid') === "" ) { ctx.statusElement.error("The page title is invalid: " + ctx.pageName); onFailure(this); return false; // abort } // retrieve actual title of the page after normalization and redirects if ( $(xml).find('page').attr('title') ) { var resolvedName = $(xml).find('page').attr('title'); // only notify user for redirects, not normalization if ( $(xml).find('redirects').length > 0 ) { Morebits.status.info("Info", "Redirected from " + ctx.pageName + " to " + resolvedName ); } ctx.pageName = resolvedName; // always update in case of normalization } else { // could be a circular redirect or other problem ctx.statusElement.error("Could not resolve redirects for: " + ctx.pageName); onFailure(this); // force error to stay on the screen ++Morebits.wiki.numberOfActionsLeft; return false; // abort } return true; // all OK }; // callback from saveApi.post() var fnSaveSuccess = function() { ctx.editMode = 'all'; // cancel append/prepend/revert modes var xml = ctx.saveApi.getXML(); // see if the API thinks we were successful if ($(xml).find('edit').attr('result') === "Success") { // real success // default on success action - display link for edited page var link = document.createElement('a'); link.setAttribute('href', mw.util.getUrl(ctx.pageName) ); link.appendChild(document.createTextNode(ctx.pageName)); ctx.statusElement.info(['completed (', link, ')']); if (ctx.onSaveSuccess) { ctx.onSaveSuccess(this); // invoke callback } return; } // errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki // Wikimedia wikis should only return spam blacklist errors and captchas var blacklist = $(xml).find('edit').attr('spamblacklist'); if (blacklist) { var code = document.createElement('code'); code.style.fontFamily = "monospace"; code.appendChild(document.createTextNode(blacklist)); ctx.statusElement.error(['Could not save the page because the URL ', code, ' is on the spam blacklist.']); } else if ( $(xml).find('captcha').length > 0 ) { ctx.statusElement.error("Could not save the page because the wiki server wanted you to fill out a CAPTCHA."); } else { ctx.statusElement.error("Unknown error received from API while saving page"); } // force error to stay on the screen ++Morebits.wiki.numberOfActionsLeft; ctx.onSaveFailure(this); }; // callback from saveApi.post() var fnSaveError = function() { var errorCode = ctx.saveApi.getErrorCode(); // check for edit conflict if ( errorCode === "editconflict" && ctx.conflictRetries++ < ctx.maxConflictRetries ) { // edit conflicts can occur when the page needs to be purged from the server cache var purgeQuery = { action: 'purge', titles: ctx.pageName // redirects are already resolved }; var purgeApi = new Morebits.wiki.api("Edit conflict detected, purging server cache", purgeQuery, null, ctx.statusElement); var result = purgeApi.post( { async: false } ); // just wait for it, result is for debugging --Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds ctx.statusElement.info("Edit conflict detected, reapplying edit"); ctx.loadApi.post(); // reload the page and reapply the edit // check for loss of edit token // it's impractical to request a new token here, so invoke edit conflict logic when this happens } else if ( errorCode === "notoken" && ctx.conflictRetries++ < ctx.maxConflictRetries ) { ctx.statusElement.info("Edit token is invalid, retrying"); --Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds ctx.loadApi.post(); // reload // check for network or server error } else if ( errorCode === "undefined" && ctx.retries++ < ctx.maxRetries ) { // the error might be transient, so try again ctx.statusElement.info("Save failed, retrying"); --Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds ctx.saveApi.post(); // give it another go! // hard error, give up } else { // non-admin attempting to edit a protected page - this gives a friendlier message than the default if ( errorCode === "protectedpage" ) { ctx.statusElement.error( "Failed to save edit: Page is fully protected" ); } else { ctx.statusElement.error( "Failed to save edit: " + ctx.saveApi.getErrorText() ); } ctx.editMode = 'all'; // cancel append/prepend/revert modes if (ctx.onSaveFailure) { ctx.onSaveFailure(this); // invoke callback } } }; var fnLookupCreatorSuccess = function() { var xml = ctx.lookupCreatorApi.getXML(); if ( !fnCheckPageName(xml) ) { return; // abort } ctx.creator = $(xml).find('rev').attr('user'); if (!ctx.creator) { ctx.statusElement.error("Could not find name of page creator"); return; } ctx.onLookupCreatorSuccess(this); }; var fnProcessMove = function() { var xml = ctx.moveApi.getXML(); if ($(xml).find('page').attr('missing') === "") { ctx.statusElement.error("Cannot move the page, because it no longer exists"); ctx.onMoveFailure(this); return; } // extract protection info if (Morebits.userIsInGroup('sysop')) { var editprot = $(xml).find('pr[type="edit"]'); if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to move the fully protected page "' + ctx.pageName + (editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) + '. \n\nClick OK to proceed with the move, or Cancel to skip this move.')) { ctx.statusElement.error("Move of fully protected page was aborted."); ctx.onMoveFailure(this); return; } } var moveToken = $(xml).find('tokens').attr('csrftoken'); if (!moveToken) { ctx.statusElement.error("Failed to retrieve move token."); ctx.onMoveFailure(this); return; } var query = { 'action': 'move', 'from': $(xml).find('page').attr('title'), 'to': ctx.moveDestination, 'token': moveToken, 'reason': ctx.editSummary }; if (ctx.moveTalkPage) { query.movetalk = 'true'; } if (ctx.moveSubpages) { query.movesubpages = 'true'; // XXX don't know whether this works for non-admins } if (ctx.moveSuppressRedirect) { query.noredirect = 'true'; } if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.moveProcessApi = new Morebits.wiki.api("moving page...", query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure); ctx.moveProcessApi.setParent(this); ctx.moveProcessApi.post(); }; var fnProcessDelete = function() { var xml = ctx.deleteApi.getXML(); if ($(xml).find('page').attr('missing') === "") { ctx.statusElement.error("Cannot delete the page, because it no longer exists"); ctx.onDeleteFailure(this); return; } // extract protection info var editprot = $(xml).find('pr[type="edit"]'); if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to delete the fully protected page "' + ctx.pageName + (editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) + '. \n\nClick OK to proceed with the deletion, or Cancel to skip this deletion.')) { ctx.statusElement.error("Deletion of fully protected page was aborted."); ctx.onDeleteFailure(this); return; } var deleteToken = $(xml).find('tokens').attr('csrftoken'); if (!deleteToken) { ctx.statusElement.error("Failed to retrieve delete token."); ctx.onDeleteFailure(this); return; } var query = { 'action': 'delete', 'title': $(xml).find('page').attr('title'), 'token': deleteToken, 'reason': ctx.editSummary }; if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.deleteProcessApi = new Morebits.wiki.api("deleting page...", query, ctx.onDeleteSuccess, ctx.statusElement, ctx.onDeleteFailure); ctx.deleteProcessApi.setParent(this); ctx.deleteProcessApi.post(); }; var fnProcessProtect = function() { var xml = ctx.protectApi.getXML(); var missing = ($(xml).find('page').attr('missing') === ""); if (((ctx.protectEdit || ctx.protectMove) && missing)) { ctx.statusElement.error("Cannot protect the page, because it no longer exists"); ctx.onProtectFailure(this); return; } if (ctx.protectCreate && !missing) { ctx.statusElement.error("Cannot create protect the page, because it already exists"); ctx.onProtectFailure(this); return; } // TODO cascading protection not possible on edit<sysop var protectToken = $(xml).find('tokens').attr('csrftoken'); if (!protectToken) { ctx.statusElement.error("Failed to retrieve protect token."); ctx.onProtectFailure(this); return; } // fetch existing protection levels var prs = $(xml).find('pr'); var editprot = prs.filter('[type="edit"]'); var moveprot = prs.filter('[type="move"]'); var createprot = prs.filter('[type="create"]'); var protections = [], expirys = []; // set edit protection level if (ctx.protectEdit) { protections.push('edit=' + ctx.protectEdit.level); expirys.push(ctx.protectEdit.expiry); } else if (editprot.length) { protections.push('edit=' + editprot.attr("level")); expirys.push(editprot.attr("expiry").replace("infinity", "indefinite")); } if (ctx.protectMove) { protections.push('move=' + ctx.protectMove.level); expirys.push(ctx.protectMove.expiry); } else if (moveprot.length) { protections.push('move=' + moveprot.attr("level")); expirys.push(moveprot.attr("expiry").replace("infinity", "indefinite")); } if (ctx.protectCreate) { protections.push('create=' + ctx.protectCreate.level); expirys.push(ctx.protectCreate.expiry); } else if (createprot.length) { protections.push('create=' + createprot.attr("level")); expirys.push(createprot.attr("expiry").replace("infinity", "indefinite")); } var query = { action: 'protect', title: $(xml).find('page').attr('title'), token: protectToken, protections: protections.join('|'), expiry: expirys.join('|'), reason: ctx.editSummary }; if (ctx.protectCascade) { query.cascade = 'true'; } if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.protectProcessApi = new Morebits.wiki.api("protecting page...", query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure); ctx.protectProcessApi.setParent(this); ctx.protectProcessApi.post(); }; var fnProcessStabilize = function() { var xml = ctx.stabilizeApi.getXML(); var missing = ($(xml).find('page').attr('missing') === ""); if (missing) { ctx.statusElement.error("Cannot protect the page, because it no longer exists"); ctx.onStabilizeFailure(this); return; } var stabilizeToken = $(xml).find('tokens').attr('csrftoken'); if (!stabilizeToken) { ctx.statusElement.error("Failed to retrieve stabilize token."); ctx.onStabilizeFailure(this); return; } var query = { action: 'stabilize', title: $(xml).find('page').attr('title'), token: stabilizeToken, protectlevel: ctx.flaggedRevs.level, expiry: ctx.flaggedRevs.expiry, reason: ctx.editSummary }; if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.stabilizeProcessApi = new Morebits.wiki.api("configuring stabilization settings...", query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure); ctx.stabilizeProcessApi.setParent(this); ctx.stabilizeProcessApi.post(); }; }; // end Morebits.wiki.page /** Morebits.wiki.page TODO: (XXX) * - Should we retry loads also? * - Need to reset current action before the save? * - Deal with action.completed stuff * - Need to reset all parameters once done (e.g. edit summary, move destination, etc.) */ /** * **************** Morebits.wiki.preview **************** * Uses the API to parse a fragment of wikitext and render it as HTML. * * Constructor: Morebits.wiki.preview(previewbox, currentAction) * previewbox - the <div> element that will contain the rendered HTML * * beginRender(wikitext): Displays the preview box, and begins an asynchronous attempt * to render the specified wikitext. * wikitext - wikitext to render; most things should work, including subst: and ~~~~ * * closePreview(): Hides the preview box and clears it. * * The suggested implementation pattern (in Morebits.simpleWindow + Morebits.quickForm situations) is to * construct a Morebits.wiki.preview object after rendering a Morebits.quickForm, and bind the object * to an arbitrary property of the form (e.g. |previewer|). For an example, see * twinklewarn.js. */ Morebits.wiki.preview = function(previewbox) { this.previewbox = previewbox; $(previewbox).addClass("morebits-previewbox").hide(); this.beginRender = function(wikitext) { $(previewbox).show(); var statusspan = document.createElement('span'); previewbox.appendChild(statusspan); Morebits.status.init(statusspan); var query = { action: 'parse', prop: 'text', pst: 'true', // PST = pre-save transform; this makes substitution work properly text: wikitext, title: mw.config.get('wgPageName') }; var renderApi = new Morebits.wiki.api("loading...", query, fnRenderSuccess, new Morebits.status("Preview")); renderApi.post(); }; var fnRenderSuccess = function(apiobj) { var xml = apiobj.getXML(); var html = $(xml).find('text').text(); if (!html) { apiobj.statelem.error("failed to retrieve preview, or template was blanked"); return; } previewbox.innerHTML = html; }; this.closePreview = function() { $(previewbox).empty().hide(); }; }; /** * **************** Morebits.wikitext **************** * Wikitext manipulation */ Morebits.wikitext = {}; Morebits.wikitext.template = { parse: function( text, start ) { var count = -1; var level = -1; var equals = -1; var current = ''; var result = { name: '', parameters: {} }; var key, value; for( var i = start; i < text.length; ++i ) { var test3 = text.substr( i, 3 ); if( test3 === '{{{' ) { current += '{{{'; i += 2; ++level; continue; } if( test3 === '}}}' ) { current += '}}}'; i += 2; --level; continue; } var test2 = text.substr( i, 2 ); if( test2 === '{{' || test2 === '[[' ) { current += test2; ++i; ++level; continue; } if( test2 === '[[' ) { current += test2; ++i; --level; continue; } if( test2 === '}}' ) { current += test2; ++i; --level; if( level <= 0 ) { if( count === -1 ) { result.name = current.substring(2).trim(); ++count; } else { if( equals !== -1 ) { key = current.substring( 0, equals ).trim(); value = current.substring( equals ).trim(); result.parameters[key] = value; equals = -1; } else { result.parameters[count] = current; ++count; } } break; } continue; } if( text.charAt(i) === '|' && level <= 0 ) { if( count === -1 ) { result.name = current.substring(2).trim(); ++count; } else { if( equals !== -1 ) { key = current.substring( 0, equals ).trim(); value = current.substring( equals + 1 ).trim(); result.parameters[key] = value; equals = -1; } else { result.parameters[count] = current; ++count; } } current = ''; } else if( equals === -1 && text.charAt(i) === '=' && level <= 0 ) { equals = current.length; current += text.charAt(i); } else { current += text.charAt(i); } } return result; } }; Morebits.wikitext.page = function mediawikiPage( text ) { this.text = text; }; Morebits.wikitext.page.prototype = { text: '', removeLink: function( link_target ) { var first_char = link_target.substr( 0, 1 ); var link_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( link_target.substr( 1 ), true ); var link_simple_re = new RegExp( "\\[\\[:?(" + link_re_string + ")\\]\\]", 'g' ); var link_named_re = new RegExp( "\\[\\[:?" + link_re_string + "\\|(.+?)\\]\\]", 'g' ); this.text = this.text.replace( link_simple_re, "$1" ).replace( link_named_re, "$1" ); }, commentOutImage: function( image, reason ) { var unbinder = new Morebits.unbinder( this.text ); unbinder.unbind( '<!--', '-->' ); reason = reason ? (reason + ': ') : ''; var first_char = image.substr( 0, 1 ); var image_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( image.substr( 1 ), true ); /* * Check for normal image links, i.e. [[Image:Foobar.png|...]] * Will eat the whole link */ var links_re = new RegExp( "\\[\\[(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string ); var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( unbinder.content, '[[', ']]' )); for( var i = 0; i < allLinks.length; ++i ) { if( links_re.test( allLinks[i] ) ) { var replacement = '<!-- ' + reason + allLinks[i] + ' -->'; unbinder.content = unbinder.content.replace( allLinks[i], replacement, 'g' ); } } // unbind the newly created comments unbinder.unbind( '<!--', '-->' ); /* * Check for gallery images, i.e. instances that must start on a new line, eventually preceded with some space, and must include Image: prefix * Will eat the whole line. */ var gallery_image_re = new RegExp( "(^\\s*(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string + ".*?$)", 'mg' ); unbinder.content.replace( gallery_image_re, "<!-- " + reason + "$1 -->" ); // unbind the newly created comments unbinder.unbind( '<!--', '-->' ); /* * Check free image usages, for example as template arguments, might have the Image: prefix excluded, but must be preceeded by an | * Will only eat the image name and the preceeding bar and an eventual named parameter */ var free_image_re = new RegExp( "(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:(?:[Ii]mage|[Ff]ile):\\s*)?" + image_re_string + ")", 'mg' ); unbinder.content.replace( free_image_re, "<!-- " + reason + "$1 -->" ); // Rebind the content now, we are done! this.text = unbinder.rebind(); }, addToImageComment: function( image, data ) { var first_char = image.substr( 0, 1 ); var first_char_regex = RegExp.escape( first_char, true ); if( first_char.toUpperCase() !== first_char.toLowerCase() ) { first_char_regex = '[' + RegExp.escape( first_char.toUpperCase(), true ) + RegExp.escape( first_char.toLowerCase(), true ) + ']'; } var image_re_string = "(?:[Ii]mage|[Ff]ile):\\s*" + first_char_regex + RegExp.escape( image.substr( 1 ), true ); var links_re = new RegExp( "\\[\\[" + image_re_string ); var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '[[', ']]' )); for( var i = 0; i < allLinks.length; ++i ) { if( links_re.test( allLinks[i] ) ) { var replacement = allLinks[i]; // just put it at the end? replacement = replacement.replace( /\]\]$/, '|' + data + ']]' ); this.text = this.text.replace( allLinks[i], replacement, 'g' ); } } var gallery_re = new RegExp( "^(\\s*" + image_re_string + '.*?)\\|?(.*?)$', 'mg' ); var newtext = "$1|$2 " + data; this.text = this.text.replace( gallery_re, newtext ); }, removeTemplate: function( template ) { var first_char = template.substr( 0, 1 ); var template_re_string = "(?:[Tt]emplate:)?\\s*[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( template.substr( 1 ), true ); var links_re = new RegExp( "\\{\\{" + template_re_string ); var allTemplates = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '{{', '}}', [ '{{{', '}}}' ] )); for( var i = 0; i < allTemplates.length; ++i ) { if( links_re.test( allTemplates[i] ) ) { this.text = this.text.replace( allTemplates[i], '', 'g' ); } } }, getText: function() { return this.text; } }; /** * **************** Morebits.queryString **************** * Maps the querystring to an object * * Functions: * * Morebits.queryString.exists(key) * returns true if the particular key is set * Morebits.queryString.get(key) * returns the value associated to the key * Morebits.queryString.equals(key, value) * returns true if the value associated with given key equals given value * Morebits.queryString.toString() * returns the query string as a string * Morebits.queryString.create( hash ) * creates an querystring and encodes strings via encodeURIComponent and joins arrays with | * * In static context, the value of location.search.substring(1), else the value given to the constructor is going to be used. The mapped hash is saved in the object. * * Example: * * var value = Morebits.queryString.get('key'); * var obj = new Morebits.queryString('foo=bar&baz=quux'); * value = obj.get('foo'); */ Morebits.queryString = function QueryString(qString) { this.string = qString; this.params = {}; if( !qString.length ) { return; } qString.replace(/\+/, ' '); var args = qString.split('&'); for( var i = 0; i < args.length; ++i ) { var pair = args[i].split( '=' ); var key = decodeURIComponent( pair[0] ), value = key; if( pair.length === 2 ) { value = decodeURIComponent( pair[1] ); } this.params[key] = value; } }; Morebits.queryString.staticstr = null; Morebits.queryString.staticInit = function() { if( !Morebits.queryString.staticstr ) { Morebits.queryString.staticstr = new Morebits.queryString(location.search.substring(1)); } }; Morebits.queryString.get = function(key) { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.get(key); }; Morebits.queryString.prototype.get = function(key) { return this.params[key] ? this.params[key] : null; }; Morebits.queryString.exists = function(key) { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.exists(key); }; Morebits.queryString.prototype.exists = function(key) { return this.params[key] ? true : false; }; Morebits.queryString.equals = function(key, value) { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.equals(key, value); }; Morebits.queryString.prototype.equals = function(key, value) { return this.params[key] === value ? true : false; }; Morebits.queryString.toString = function() { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.toString(); }; Morebits.queryString.prototype.toString = function() { return this.string ? this.string : null; }; Morebits.queryString.create = function( arr ) { var resarr = []; var editToken; // KLUGE: this should always be the last item in the query string (bug TW-B-0013) for( var i in arr ) { if( arr[i] === undefined ) { continue; } var res; if( $.isArray( arr[i] ) ){ var v = []; for(var j = 0; j < arr[i].length; ++j ) { v[j] = encodeURIComponent( arr[i][j] ); } res = v.join('|'); } else { res = encodeURIComponent( arr[i] ); } if( i === 'token' ) { editToken = res; } else { resarr.push( encodeURIComponent( i ) + '=' + res ); } } if( editToken !== undefined ) { resarr.push( 'token=' + editToken ); } return resarr.join('&'); }; Morebits.queryString.prototype.create = Morebits.queryString.create; /** * **************** Morebits.status **************** */ Morebits.status = function Status( text, stat, type ) { this.textRaw = text; this.text = this.codify(text); this.type = type || 'status'; this.generate(); if( stat ) { this.update( stat, type ); } }; Morebits.status.init = function( root ) { if( !( root instanceof Element ) ) { throw new Error( 'object not an instance of Element' ); } while( root.hasChildNodes() ) { root.removeChild( root.firstChild ); } Morebits.status.root = root; Morebits.status.errorEvent = null; }; Morebits.status.root = null; Morebits.status.onError = function( handler ) { if ( $.isFunction( handler ) ) { Morebits.status.errorEvent = handler; } else { throw "Morebits.status.onError: handler is not a function"; } }; Morebits.status.prototype = { stat: null, text: null, textRaw: null, type: 'status', target: null, node: null, linked: false, link: function() { if( ! this.linked && Morebits.status.root ) { Morebits.status.root.appendChild( this.node ); this.linked = true; } }, unlink: function() { if( this.linked ) { Morebits.status.root.removeChild( this.node ); this.linked = false; } }, codify: function( obj ) { if ( ! $.isArray( obj ) ) { obj = [ obj ]; } var result; result = document.createDocumentFragment(); for( var i = 0; i < obj.length; ++i ) { if( typeof obj[i] === 'string' ) { result.appendChild( document.createTextNode( obj[i] ) ); } else if( obj[i] instanceof Element ) { result.appendChild( obj[i] ); } // Else cosmic radiation made something shit } return result; }, update: function( status, type ) { this.stat = this.codify( status ); if( type ) { this.type = type; if (type === 'error') { // hack to force the page not to reload when an error is output - see also Morebits.status() above Morebits.wiki.numberOfActionsLeft = 1000; // call error callback if (Morebits.status.errorEvent) { Morebits.status.errorEvent(); } // also log error messages in the browser console if (console && console.error) { console.error(this.textRaw + ": " + status); } } } this.render(); }, generate: function() { this.node = document.createElement( 'div' ); this.node.appendChild( document.createElement('span') ).appendChild( this.text ); this.node.appendChild( document.createElement('span') ).appendChild( document.createTextNode( ': ' ) ); this.target = this.node.appendChild( document.createElement( 'span' ) ); this.target.appendChild( document.createTextNode( '' ) ); // dummy node }, render: function() { this.node.className = 'tw_status_' + this.type; while( this.target.hasChildNodes() ) { this.target.removeChild( this.target.firstChild ); } this.target.appendChild( this.stat ); this.link(); }, status: function( status ) { this.update( status, 'status'); }, info: function( status ) { this.update( status, 'info'); }, warn: function( status ) { this.update( status, 'warn'); }, error: function( status ) { this.update( status, 'error'); } }; Morebits.status.info = function( text, status ) { return new Morebits.status( text, status, 'info' ); }; Morebits.status.warn = function( text, status ) { return new Morebits.status( text, status, 'warn' ); }; Morebits.status.error = function( text, status ) { return new Morebits.status( text, status, 'error' ); }; /** * **************** Morebits.htmlNode() **************** * Simple helper function to create a simple node */ Morebits.htmlNode = function ( type, content, color ) { var node = document.createElement( type ); if( color ) { node.style.color = color; } node.appendChild( document.createTextNode( content ) ); return node; } /** * **************** Morebits.simpleWindow **************** * A simple draggable window * now a wrapper for jQuery UI's dialog feature */ // The height passed in here is the maximum allowable height for the content area. Morebits.simpleWindow = function SimpleWindow( width, height ) { var content = document.createElement( 'div' ); this.content = content; content.className = 'morebits-dialog-content'; this.height = height; $(this.content).dialog({ autoOpen: false, buttons: { "Placeholder button": function() {} }, dialogClass: 'morebits-dialog', width: Math.min(parseInt(window.innerWidth, 10), parseInt(width ? width : 800, 10)), // give jQuery the given height value (which represents the anticipated height of the dialog) here, so // it can position the dialog appropriately // the 20 pixels represents adjustment for the extra height of the jQuery dialog "chrome", compared // to that of the old SimpleWindow height: height + 20, close: function(event, ui) { // dialogs and their content can be destroyed once closed $(event.target).dialog("destroy").remove(); }, resize: function(event, ui) { this.style.maxHeight = ""; } }); var $widget = $(this.content).dialog("widget"); // add background gradient to titlebar var $titlebar = $widget.find(".ui-dialog-titlebar"); var oldstyle = $titlebar.attr("style"); $titlebar.attr("style", (oldstyle ? oldstyle : "") + '; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC) !important;'); // delete the placeholder button (it's only there so the buttonpane gets created) $widget.find("button").each(function(key, value) { value.parentNode.removeChild(value); }); // add container for the buttons we add, and the footer links (if any) var buttonspan = document.createElement("span"); buttonspan.className = "morebits-dialog-buttons"; var linksspan = document.createElement("span"); linksspan.className = "morebits-dialog-footerlinks"; $widget.find(".ui-dialog-buttonpane").append(buttonspan, linksspan); }; Morebits.simpleWindow.prototype = { buttons: [], height: 600, hasFooterLinks: false, scriptName: null, // Focuses the dialog. This might work, or on the contrary, it might not. focus: function(event) { $(this.content).dialog("moveToTop"); return this; }, // Closes the dialog. If this is set as an event handler, it will stop the event from doing anything more. close: function(event) { if (event) { event.preventDefault(); } $(this.content).dialog("close"); return this; }, // Shows the dialog. Calling display() on a dialog that has previously been closed might work, but it is not guaranteed. display: function() { if (this.scriptName) { var $widget = $(this.content).dialog("widget"); $widget.find(".morebits-dialog-scriptname").remove(); var scriptnamespan = document.createElement("span"); scriptnamespan.className = "morebits-dialog-scriptname"; scriptnamespan.textContent = this.scriptName + " \u00B7 "; // U+00B7 MIDDLE DOT = &middot; $widget.find(".ui-dialog-title").prepend(scriptnamespan); } var dialog = $(this.content).dialog("open"); if (window.setupTooltips && window.pg && window.pg.re && window.pg.re.diff) { // tie in with NAVPOP dialog.parent()[0].ranSetupTooltipsAlready = false; setupTooltips(dialog.parent()[0]); } this.setHeight( this.height ); // init height algorithm return this; }, // Sets the dialog title. setTitle: function( title ) { $(this.content).dialog("option", "title", title); return this; }, // Sets the script name, appearing as a prefix to the title to help users determine which // user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle". setScriptName: function( name ) { this.scriptName = name; return this; }, // Sets the dialog width. setWidth: function( width ) { $(this.content).dialog("option", "width", width); return this; }, // Sets the dialog's maximum height. The dialog will auto-size to fit its contents, // but the content area will grow no larger than the height given here. setHeight: function( height ) { this.height = height; // from display time onwards, let the browser determine the optimum height, and instead limit the height at the given value // note that the given height will exclude the approx. 20px that the jQuery UI chrome has in height in addition to the height // of an equivalent "classic" Morebits.simpleWindow if (parseInt(getComputedStyle($(this.content).dialog("widget")[0], null).height, 10) > window.innerHeight) { $(this.content).dialog("option", "height", window.innerHeight - 2).dialog("option", "position", "top"); } else { $(this.content).dialog("option", "height", "auto"); } $(this.content).dialog("widget").find(".morebits-dialog-content")[0].style.maxHeight = parseInt(this.height - 30, 10) + "px"; return this; }, // Sets the content of the dialog to the given element node, usually from rendering a Morebits.quickForm. // Re-enumerates the footer buttons, but leaves the footer links as they are. // Be sure to call this at least once before the dialog is displayed... setContent: function( content ) { this.purgeContent(); this.addContent( content ); return this; }, addContent: function( content ) { this.content.appendChild( content ); // look for submit buttons in the content, hide them, and add a proxy button to the button pane var thisproxy = this; $(this.content).find('input[type="submit"], button[type="submit"]').each(function(key, value) { value.style.display = "none"; var button = document.createElement("button"); button.textContent = (value.hasAttribute("value") ? value.getAttribute("value") : (value.textContent ? value.textContent : "Submit Query")); // here is an instance of cheap coding, probably a memory-usage hit in using a closure here button.addEventListener("click", function() { value.click(); }, false); thisproxy.buttons.push(button); }); // remove all buttons from the button pane and re-add them if (this.buttons.length > 0) { $(this.content).dialog("widget").find(".morebits-dialog-buttons").empty().append(this.buttons)[0].removeAttribute("data-empty"); } else { $(this.content).dialog("widget").find(".morebits-dialog-buttons")[0].setAttribute("data-empty", "data-empty"); // used by CSS } return this; }, purgeContent: function( content ) { this.buttons = []; // delete all buttons in the buttonpane $(this.content).dialog("widget").find(".morebits-dialog-buttons").empty(); while( this.content.hasChildNodes() ) { this.content.removeChild( this.content.firstChild ); } return this; }, // Adds a link in the bottom-right corner of the dialog. // This can be used to provide help or policy links. // For example, Twinkle's CSD module adds a link to the CSD policy page, // as well as a link to Twinkle's documentation. addFooterLink: function( text, wikiPage ) { var $footerlinks = $(this.content).dialog("widget").find(".morebits-dialog-footerlinks"); if (this.hasFooterLinks) { var bullet = document.createElement("span"); bullet.textContent = " \u2022 "; // U+2022 BULLET $footerlinks.append(bullet); } var link = document.createElement("a"); link.setAttribute("href", mw.util.getUrl(wikiPage) ); link.setAttribute("title", wikiPage); link.setAttribute("target", "_blank"); link.textContent = text; $footerlinks.append(link); this.hasFooterLinks = true; return this; }, setModality: function( modal ) { $(this.content).dialog("option", "modal", modal); return this; } }; // Enables or disables all footer buttons on all Morebits.simpleWindows in the current page. // This should be called with |false| when the button(s) become irrelevant (e.g. just before Morebits.status.init is called). // This is not an instance method so that consumers don't have to keep a reference to the original // Morebits.simpleWindow object sitting around somewhere. Anyway, most of the time there will only be one // Morebits.simpleWindow open, so this shouldn't matter. Morebits.simpleWindow.setButtonsEnabled = function( enabled ) { $(".morebits-dialog-buttons button").attr("disabled", !enabled); }; // Twinkle blacklist was removed per consensus at http://en.wikipedia.org/wiki/Wikipedia:Administrators%27_noticeboard/Archive221#New_Twinkle_blacklist_proposal } ( window, document, jQuery )); // End wrap with anonymous function /** * If this script is being executed outside a ResourceLoader context, we add some * global assignments for legacy scripts, hopefully these can be removed down the line * * IMPORTANT NOTE: * PLEASE DO NOT USE THESE ALIASES IN NEW CODE! * Thanks. */ if ( typeof arguments === "undefined" ) { // typeof is here for a reason... window.SimpleWindow = Morebits.simpleWindow; window.QuickForm = Morebits.quickForm; window.Wikipedia = Morebits.wiki; window.Status = Morebits.status; window.QueryString = Morebits.queryString; } // </nowiki> // [[Category:Scripts]] // [[Category:Twinkle]] be7ce576a8bd7447153c70167bdc568d1cc37459 Template:If empty 10 98 187 2023-03-21T04:15:21Z dev>Pppery 0 Rv wikitext text/x-wiki <includeonly>{{{{{|safesubst:}}}#if:{{{1|}}} | {{{1}}} | {{{{{|safesubst:}}}#if:{{{2|}}} | {{{2}}} | {{{{{|safesubst:}}}#if:{{{3|}}} | {{{3}}} | {{{{{|safesubst:}}}#if:{{{4|}}} | {{{4}}} | {{{{{|safesubst:}}}#if:{{{5|}}} | {{{5}}} | {{{{{|safesubst:}}}#if:{{{6|}}} | {{{6}}} | {{{{{|safesubst:}}}#if:{{{7|}}} | {{{7}}} | {{{{{|safesubst:}}}#if:{{{8|}}} | {{{8}}} | {{{{{|safesubst:}}}#if:{{{9|}}} | {{{9}}} }} }} }} }} }} }} }} }} }}</includeonly><noinclude> {{Documentation}} <!-- Add categories and interwikis to the /doc subpage, not here! --> </noinclude> eeda2c13231e9a8b44d480e8c429d73652575009 Template:Infobox character 10 16 25 2023-03-21T04:32:31Z dev>Pppery 0 wikitext text/x-wiki {{Infobox | bodystyle = border-spacing: 2px 5px; | above = {{If empty |{{{name|}}} |<includeonly>{{PAGENAMEBASE}}</includeonly> }} | abovestyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} | color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }} | subheader = {{#if: {{{series|}}}{{{franchise|}}} | {{#if: {{{series|}}} | ''{{{series|}}}'' | {{{franchise|}}} }} character{{#if: {{{multiple|}}} | s }} }} | image = {{{image|}}}| | caption = {{{caption|}}} | headerstyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} |color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }} | label1 = First appearance | data1 = {{#if: {{{first|}}} | {{{first|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{first_major|}}} |minor_work={{{first_minor|}}} |issue={{{first_issue|}}} |date={{{first_date|}}} }} }} | label2 = First game | data2 = {{{firstgame|}}} | label3 = Last appearance | data3 = {{#if: {{{last|}}} | {{{last|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{last_major|}}} |minor_work={{{last_minor|}}} |issue={{{last_issue|}}} |date={{{last_date|}}} }} }} | label4 = Created by | data4 = {{{creator|}}} | label5 = Based on | data5 = {{{based_on|}}} | label6 = Adapted by | data6 = {{{adapted_by|}}} | label7 = Designed by | data7 = {{{designer|}}} | label8 = Portrayed by | data8 = {{{portrayer|}}} | label9 = Voiced by | data9 = {{{voice|}}} | label10 = Motion capture | data10 = {{{motion_actor|}}} | label11 = {{{lbl1|}}} | data11 = {{{data1|}}} | label12 = {{{lbl2|}}} | data12 = {{{data2|}}} | label13 = {{{lbl3|}}} | data13 = {{{data3|}}} | label14 = {{{lbl4|}}} | data14 = {{{data4|}}} | label15 = {{{lbl5|}}} | data15 = {{{data5|}}} | header20 = {{#if: {{{noinfo|}}} || {{#if: {{{full_name|}}} {{{alias|}}} {{{aliases|}}} {{{nickname|}}} {{{nicknames|}}} {{{race|}}} {{{species|}}} {{{gender|}}} {{{title|}}} {{{occupation|}}} {{{position|}}} {{{class|}}} {{{affiliation|}}} {{{alignment|}}} {{{fighting_style|}}} {{{weapon|}}} {{{family|}}} {{{spouse|}}} {{{spouses|}}} {{{significant_other|}}} {{{significant_others|}}} {{{children|}}} {{{relatives|}}} {{{religion|}}} {{{origin|}}} {{{home|}}} {{{nationality|}}} {{{data21|}}} {{{data22|}}} {{{data23|}}} {{{data24|}}} {{{data25|}}} | {{If empty |{{{info-hdr|}}} |In-universe information }} }} }} | label21 = Full name | data21 = {{{full_name|}}} | label22 = {{#if: {{{alias|}}} | Alias | Aliases }} | data22 = {{If empty |{{{alias|}}} |{{{aliases|}}} }} | label23 = {{#if: {{{nickname|}}} | Nickname | Nicknames }} | data23 = {{If empty |{{{nickname|}}} |{{{nicknames|}}} }} | label24 = {{#if: {{{race|}}} | Race | Species }} | data24 = {{If empty |{{{race|}}} |{{{species|}}} }} | label25 = Gender | data25 = {{{gender|}}} | label26 = Title | data26 = {{{title|}}} | label27 = {{#if: {{{occupation|}}} | Occupation | {{#if: {{{position|}}} | Position | Class }} }} | data27 = {{If empty |{{{occupation|}}} |{{{position|}}} |{{{class|}}} }} | label28 = {{#if: {{{affiliation|}}} | Affiliation | Alignment }} | data28 = {{If empty |{{{affiliation|}}} |{{{alignment|}}} }} | label29 = Fighting style | data29 = {{{fighting_style|}}} | label30 = Weapon | data30 = {{{weapon|}}} | label31 = Family | data31 = {{{family|}}} | label32 = {{#if: {{{spouse|}}} | Spouse | Spouses }} | data32 = {{If empty |{{{spouse|}}} |{{{spouses|}}} }} | label33 = {{#if: {{{significant_other|}}} | Significant other | Significant others }} | data33 = {{If empty |{{{significant_other|}}} |{{{significant_others|}}} }} | label34 = Children | data34 = {{{children|}}} | label35 = Relatives | data35 = {{{relatives|}}} | label36 = Religion | data36 = {{{religion|}}} | label37 = {{#if: {{{origin|}}} | Origin | Home }} | data37 = {{If empty |{{{origin|}}} |{{{home|}}} }} | label38 = Nationality | data38 = {{{nationality|}}} | label39 = {{{lbl21|}}} | data39 = {{{data21|}}} | label40 = {{{lbl22|}}} | data40 = {{{data22|}}} | label41 = {{{lbl23|}}} | data41 = {{{data23|}}} | label42 = {{{lbl24|}}} | data42 = {{{data24|}}} | label43 = {{{lbl25|}}} | data43 = {{{data25|}}} | header50 = {{#if: {{{data31|}}} {{{data32|}}} {{{data33|}}} {{{data34|}}} {{{data35|}}} | {{{extra-hdr|}}} }} | label51 = {{{lbl31|}}} | data51 = {{{data31|}}} | label52 = {{{lbl32|}}} | data52 = {{{data32|}}} | label53 = {{{lbl33|}}} | data53 = {{{data33|}}} | label54 = {{{lbl34|}}} | data54 = {{{data34|}}} | label55 = {{{lbl35|}}} | data55 = {{{data35|}}} }}<noinclude> {{Documentation}} <!-- Add categories to the /doc subpage, not here! --> </noinclude> 3ce93cfa3dfb9b4b9608b850f3d4e6ac282b3bc9 26 25 2023-08-27T19:15:56Z Alxira5 4 1 revision imported: Template imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki {{Infobox | bodystyle = border-spacing: 2px 5px; | above = {{If empty |{{{name|}}} |<includeonly>{{PAGENAMEBASE}}</includeonly> }} | abovestyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} | color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }} | subheader = {{#if: {{{series|}}}{{{franchise|}}} | {{#if: {{{series|}}} | ''{{{series|}}}'' | {{{franchise|}}} }} character{{#if: {{{multiple|}}} | s }} }} | image = {{{image|}}}| | caption = {{{caption|}}} | headerstyle = background: {{If empty |{{{color|}}} |{{{colour|}}} |#DEDEE2 }}; {{#if: {{{color|}}}{{{colour|}}} |color: {{Greater color contrast ratio|{{If empty |{{{color|}}} |{{{colour|}}} }}|black|white }}; }} | label1 = First appearance | data1 = {{#if: {{{first|}}} | {{{first|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{first_major|}}} |minor_work={{{first_minor|}}} |issue={{{first_issue|}}} |date={{{first_date|}}} }} }} | label2 = First game | data2 = {{{firstgame|}}} | label3 = Last appearance | data3 = {{#if: {{{last|}}} | {{{last|}}} | {{#invoke:Formatted appearance|getFormattedAppearance|major_work={{{last_major|}}} |minor_work={{{last_minor|}}} |issue={{{last_issue|}}} |date={{{last_date|}}} }} }} | label4 = Created by | data4 = {{{creator|}}} | label5 = Based on | data5 = {{{based_on|}}} | label6 = Adapted by | data6 = {{{adapted_by|}}} | label7 = Designed by | data7 = {{{designer|}}} | label8 = Portrayed by | data8 = {{{portrayer|}}} | label9 = Voiced by | data9 = {{{voice|}}} | label10 = Motion capture | data10 = {{{motion_actor|}}} | label11 = {{{lbl1|}}} | data11 = {{{data1|}}} | label12 = {{{lbl2|}}} | data12 = {{{data2|}}} | label13 = {{{lbl3|}}} | data13 = {{{data3|}}} | label14 = {{{lbl4|}}} | data14 = {{{data4|}}} | label15 = {{{lbl5|}}} | data15 = {{{data5|}}} | header20 = {{#if: {{{noinfo|}}} || {{#if: {{{full_name|}}} {{{alias|}}} {{{aliases|}}} {{{nickname|}}} {{{nicknames|}}} {{{race|}}} {{{species|}}} {{{gender|}}} {{{title|}}} {{{occupation|}}} {{{position|}}} {{{class|}}} {{{affiliation|}}} {{{alignment|}}} {{{fighting_style|}}} {{{weapon|}}} {{{family|}}} {{{spouse|}}} {{{spouses|}}} {{{significant_other|}}} {{{significant_others|}}} {{{children|}}} {{{relatives|}}} {{{religion|}}} {{{origin|}}} {{{home|}}} {{{nationality|}}} {{{data21|}}} {{{data22|}}} {{{data23|}}} {{{data24|}}} {{{data25|}}} | {{If empty |{{{info-hdr|}}} |In-universe information }} }} }} | label21 = Full name | data21 = {{{full_name|}}} | label22 = {{#if: {{{alias|}}} | Alias | Aliases }} | data22 = {{If empty |{{{alias|}}} |{{{aliases|}}} }} | label23 = {{#if: {{{nickname|}}} | Nickname | Nicknames }} | data23 = {{If empty |{{{nickname|}}} |{{{nicknames|}}} }} | label24 = {{#if: {{{race|}}} | Race | Species }} | data24 = {{If empty |{{{race|}}} |{{{species|}}} }} | label25 = Gender | data25 = {{{gender|}}} | label26 = Title | data26 = {{{title|}}} | label27 = {{#if: {{{occupation|}}} | Occupation | {{#if: {{{position|}}} | Position | Class }} }} | data27 = {{If empty |{{{occupation|}}} |{{{position|}}} |{{{class|}}} }} | label28 = {{#if: {{{affiliation|}}} | Affiliation | Alignment }} | data28 = {{If empty |{{{affiliation|}}} |{{{alignment|}}} }} | label29 = Fighting style | data29 = {{{fighting_style|}}} | label30 = Weapon | data30 = {{{weapon|}}} | label31 = Family | data31 = {{{family|}}} | label32 = {{#if: {{{spouse|}}} | Spouse | Spouses }} | data32 = {{If empty |{{{spouse|}}} |{{{spouses|}}} }} | label33 = {{#if: {{{significant_other|}}} | Significant other | Significant others }} | data33 = {{If empty |{{{significant_other|}}} |{{{significant_others|}}} }} | label34 = Children | data34 = {{{children|}}} | label35 = Relatives | data35 = {{{relatives|}}} | label36 = Religion | data36 = {{{religion|}}} | label37 = {{#if: {{{origin|}}} | Origin | Home }} | data37 = {{If empty |{{{origin|}}} |{{{home|}}} }} | label38 = Nationality | data38 = {{{nationality|}}} | label39 = {{{lbl21|}}} | data39 = {{{data21|}}} | label40 = {{{lbl22|}}} | data40 = {{{data22|}}} | label41 = {{{lbl23|}}} | data41 = {{{data23|}}} | label42 = {{{lbl24|}}} | data42 = {{{data24|}}} | label43 = {{{lbl25|}}} | data43 = {{{data25|}}} | header50 = {{#if: {{{data31|}}} {{{data32|}}} {{{data33|}}} {{{data34|}}} {{{data35|}}} | {{{extra-hdr|}}} }} | label51 = {{{lbl31|}}} | data51 = {{{data31|}}} | label52 = {{{lbl32|}}} | data52 = {{{data32|}}} | label53 = {{{lbl33|}}} | data53 = {{{data33|}}} | label54 = {{{lbl34|}}} | data54 = {{{data34|}}} | label55 = {{{lbl35|}}} | data55 = {{{data35|}}} }}<noinclude> {{Documentation}} <!-- Add categories to the /doc subpage, not here! --> </noinclude> 3ce93cfa3dfb9b4b9608b850f3d4e6ac282b3bc9 Module:Formatted appearance 828 89 170 2023-03-21T04:35:42Z dev>Pppery 0 Scribunto text/plain -- This module requires the use of Module:List. local p = {} -- Local function which is used to get a correctly formatted entry. -- Function checks if the array had a value added by checking the counter, -- and returns the relevant result. local function getFormattedEntry(args, counter) if (counter == 1) then -- Check if the counter stayed the same. return "" -- Nothing was added to array; Return empty string. elseif (counter == 2) then -- Check if only one value was added to the array. return args[1] -- Only one value was added to array; Return that value. else -- The array had more than one value added. return table.concat(args, "<br/>") -- Tetrieve the formatted plainlist. end end --[[ Local function which is used to format an appearance for a comic book, in the style of: Line 1: <comic book title> #<issue number> (with comic book title in italics) Line 2: <release date> For other usages, see createGenericEntry(). The function works with the following combinations: -- Only comic book title (example: "The Incredible Hulk"). -- Title and issue number (example: "The Incredible Hulk" and "181"). -- Title and release date (example: "The Incredible Hulk and "November 1974"). -- Title, issue number and release date (example: "The Incredible Hulk", "181" and "November 1974"). -- Only release date (example: "November 1974"). --]] local function createComicEntry(appearanceMajor, appearanceMinor, appearanceDate) local fullString = {} -- Variable to save the array. local counter = 1 -- Variable to save the array counter. if (appearanceMajor ~= nil) then -- Check if a comic book title was entered. if (appearanceMinor == nil) then -- A comic book title was entered; Check if a issue number was entered. fullString[counter] = appearanceMajor -- A issue was not entered; Add only the comic book title to the array. counter = counter + 1 -- Increment counter by one. else fullString[counter] = appearanceMajor .. " " .. appearanceMinor -- A issue was entered; Add both to the array. counter = counter + 1 -- Increment counter by one. end end if (appearanceDate ~= nil) then -- Check if a release date was entered. fullString[counter] = appearanceDate -- A release date was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry. end --[[ Local function which is used to format an appearance for most usages, including television, film, books, songs and games, in the style of: Line 1: <minor work title> (in quotes) (Minor works include: TV episodes, chapters, songs and game missions) Line 2: <major work title> (in italics) (Major works include: TV series, films, books, albums and games) Line 3: <release date> For comic book usages, see createComicEntry(). The function works with the following combinations: -- Only minor work title (example: "Live Together, Die Alone"). -- Minor work title and major work title (example: "Live Together, Die Alone" and "Lost"). -- Minor work title and release date (example: "Live Together, Die Alone" and "May 24, 2006"). -- Minor work title, major work title and release date (example: "Live Together, Die Alone", "Lost" and "May 24, 2006"). -- Only major work title (example: "Lost"). -- major work title and release date (example: "Lost" and "May 24, 2006"). -- Only release date (example: "May 24, 2006"). --]] local function createGenericEntry(appearanceMajor, appearanceMinor, appearanceDate) local fullString = {} -- Variable to save the array. local counter = 1 -- Variable to save the array counter. if (appearanceMinor ~= nil) then -- Check if a minor appearance was entered. fullString[counter] = appearanceMinor -- A minor appearance was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end if (appearanceMajor ~= nil) then -- Check if a major appearance was entered. fullString[counter] = appearanceMajor -- A major appearance was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end if (appearanceDate ~= nil) then -- Check if a release date was entered. fullString[counter] = appearanceDate -- A release date was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry. end -- Local function which is used to format with a hash symbol comic book issues. -- For other minor works, see getFormattedGenericMinorWork(). local function getFormattedComicMinorWorkTitle(issue) if (issue ~= nil) then -- Check if the issue is not nil. if (string.find(issue, "#")) then -- Check if the issue already has a hash symbol. return issue -- Hash symbol already present; Return issue. else local formattedString = string.gsub(issue, "%d+", "#%1") -- Hash symbol not found; Add the symbol before the issue number. return formattedString -- Return issue. end else return nil -- issue is nil; Return nil. end end -- Local function which is used to format with quotes a minor work title of most types. -- For comic book issues, see getFormattedComicMinorWork() (see [MOS:MINORWORK]). local function getFormattedGenericMinorWorkTitle(title) if (title ~= nil) then -- Check if the title is not nil. return "\"" .. title .. "\"" -- Title is not nil; Add quotes to the title. else return nil -- Title is nil; Return nil. end end -- Local function which is used to format with italics a major work title (see [MOS:MAJORWORK]). local function getFormattedMajorWorkTitle(title) if (title ~= nil) then -- Check if the title is not nil. return "''" .. title .. "''" -- Title is not nil; Add italics to the title. else return nil -- Title is nil; Return nil. end end -- Local function which does the actual main process. local function _getFormattedAppearance(args) local appearanceMajor = args['major_work'] -- Get the title of the major work. local appearanceMinor = args['minor_work'] -- Get the title of the minor work. local isComic = false -- Variable to save the status of wether the appearence is from a comic book. if (args['issue'] ~= nil) then -- Check if the comic specific issue is not nil. appearanceMinor = args['issue'] -- Issue is not nil; Get the issue number. isComic = true -- Set isComic to true. end local appearanceDate = args['date'] -- Get the release date of the minor work. local formattedAppearanceMajor = getFormattedMajorWorkTitle(appearanceMajor) -- Call getFormattedMajorWorkTitle() to get a formatted major work title. if (isComic == false) then -- Check if the appearance is a comic book appearance. -- The appearance is not a comic book appearance; local formattedAppearanceMinor = getFormattedGenericMinorWorkTitle(appearanceMinor) -- Call getFormattedGenericMinorWorkTitle() to get a formatted minor work title. return createGenericEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createGenericEntry() to create an appearance entry. else -- The appearance is a comic book appearance. local formattedAppearanceMinor = getFormattedComicMinorWorkTitle(appearanceMinor) -- Call getFormattedComicMinorWorkTitle() to get a formatted minor work title. return createComicEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createComicEntry() to create a comic book appearance entry. end end --[[ Public function which is used to format the |first_appeared= and |last_appeared= fields. The usage of this module allows for correct title formatting (see [MOS:MAJORWORK] and [MOS:MINORWORK]), and correct line breaks based on guidelines (see [WP:UBLIST]). Parameters: -- |major_work= — optional; The title of the major work the fictional element appeared in. Major works include TV series, films, books, albums and games. -- |minor_work= — optional; The title of the minor work the fictional element appeared in. Minor works include TV episodes, chapters, songs and game missions. -- |issue= — optional; The number of the comic book issue the fictional element appeared in. -- |date= — optional; The date of the publication/release of the minor work where the fictional element appeared in. --]] function p.getFormattedAppearance(frame) local getArgs = require('Module:Arguments').getArgs -- Use Module:Arguments to access module arguments. local args = getArgs(frame) -- Get the arguments sent via the template. return _getFormattedAppearance(args) -- Call _getFormattedAppearance() to perform the actual process. end return p 983d4add2379f19ec30241c0470bf9b6c4089eb2 Main Page 0 1 1 2023-07-25T19:05:26Z MediaWiki default 1 Create main page wikitext text/x-wiki __NOTOC__ == Welcome to {{SITENAME}}! == This Main Page was created automatically and it seems it hasn't been replaced yet. === For the bureaucrat(s) of this wiki === Hello, and welcome to your new wiki! Thank you for choosing Miraheze for the hosting of your wiki, we hope you will enjoy our hosting. You can immediately start working on your wiki or whenever you want. Need help? No problem! We will help you with your wiki as needed. To start, try checking out these helpful links: * [[mw:Special:MyLanguage/Help:Contents|MediaWiki guide]] (e.g. navigation, editing, deleting pages, blocking users) * [[meta:Special:MyLanguage/FAQ|Miraheze FAQ]] * [[meta:Special:MyLanguage/Request features|Request settings changes on your wiki]]. (Extensions, Skin and Logo/Favicon changes should be done through [[Special:ManageWiki]] on your wiki, see [[meta:Special:MyLanguage/ManageWiki|ManageWiki]] for more information.) ==== I still don't understand X! ==== Well, that's no problem. Even if something isn't explained in the documentation/FAQ, we are still happy to help you. You can find us here: * [[meta:Special:MyLanguage/Help center|On our own Miraheze wiki]] * On [[phab:|Phabricator]] * On [https://miraheze.org/discord Discord] * On IRC in #miraheze on irc.libera.chat ([irc://irc.libera.chat/%23miraheze direct link]; [https://web.libera.chat/?channel=#miraheze webchat]) === For visitors of this wiki === Hello, the default Main Page of this wiki (this page) has not yet been replaced by the bureaucrat(s) of this wiki. The bureaucrat(s) might still be working on a Main Page, so please check again later! 21236ac3f8d65e5563b6da6b70815ca6bf1e6616 Template:FlowMention 10 2 2 2023-07-25T19:42:04Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki @[[User:{{{1|Example}}}|{{{2|{{{1|Example}}}}}}]] 98786e33cb63444ac23e3cf2bdbcab2d9501a6e7 Template:LQT Moved thread stub converted to Flow 10 3 3 2023-07-25T19:42:05Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki This post by {{{author}}} was moved on {{{date}}}. You can find it at [[{{{title}}}]]. 792a92295d0603dc3cb5c46e15d4e42af9659414 Template:LQT page converted to Flow 10 4 4 2023-07-25T19:42:05Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki Previous page history was archived for backup purposes at <span class='flow-link-to-archive'>[[{{{archive}}}]]</span> on {{#time: Y-m-d|{{{date}}}}}. c988d60d1df8c49bcce1f4f94a9c2a5318faf0d3 Template:Archive for converted LQT page 10 5 5 2023-07-25T19:42:06Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki This page is an archived LiquidThreads page. <strong>Do not edit the contents of this page</strong>. Please direct any additional comments to the [[{{{from}}}|current talk page]]. 6f2232948be664f5eec18e4b7a6219814d38a478 Template:LQT post imported with suppressed user 10 6 6 2023-07-25T19:42:07Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki This revision was imported from LiquidThreads with a suppressed user. It has been reassigned to the current user. 0eb25fe53f4e146ddc0b16b14bd40d6069e56c06 Template:LQT post imported with different signature user 10 7 7 2023-07-25T19:42:07Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki <em>This post was posted by [[User:{{{authorUser}}}|{{{authorUser}}}]], but signed as [[User:{{{signatureUser}}}|{{{signatureUser}}}]].</em> 047294b02240e1b8526ad076eb47a07e98747bac Template:Wikitext talk page converted to Flow 10 8 8 2023-07-25T19:42:08Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki Previous discussion was archived at <span class='flow-link-to-archive'>[[{{{archive}}}]]</span> on {{#time: Y-m-d|{{{date}}}}}. 69c9712008fdef423f0f0332a7d4ffcfe65e6e76 Template:Archive for converted wikitext talk page 10 9 9 2023-07-25T19:42:08Z Flow talk page manager 3 /* Automatically created by Flow */ wikitext text/x-wiki This page is an archive. <strong>Do not edit the contents of this page</strong>. Please direct any additional comments to the [[{{{from|{{TALKSPACE}}:{{BASEPAGENAME}}}}}|current talk page]]. de059e2d945be0557b47d689299d8bd96e9699ed Special:Badtitle/NS200:AlexGX22 200 10 10 2023-07-25T21:09:56Z AlXGX21 2 import user wiki wikitext text/x-wiki da39a3ee5e6b4b0d3255bfef95601890afd80709 User:AlXGX21 2 14 23 2023-08-27T17:41:26Z AlXGX21 2 Redirection to my new account wikitext text/x-wiki #REDIRECT [[User:Alxira5|</nowiki>Alxira5]] 5cd0368e4dea8043dc1782a4946317e31e03d2a2 MediaWiki:Common.css 8 15 24 2023-08-27T18:54:13Z Alxira5 4 CSS file imported from Miraheze Developers, published under the CC-BY-SA 4.0 license css text/css /* Default style for navigation boxes */ .navbox { /* Navbox container style */ box-sizing: border-box; border: 1px solid #a2a9b1; width: 100%; clear: both; font-size: 88%; text-align: center; padding: 1px; margin: 1em auto 0; /* Prevent preceding content from clinging to navboxes */ } .navbox .navbox { margin-top: 0; /* No top margin for nested navboxes */ } .navbox + .navbox { margin-top: -1px; /* Single pixel border between adjacent navboxes */ } .navbox-inner, .navbox-subgroup { width: 100%; } .navbox-group, .navbox-title, .navbox-abovebelow { padding: 0.25em 1em; /* Title, group and above/below styles */ line-height: 1.5em; text-align: center; } th.navbox-group { /* Group style */ white-space: nowrap; /* @noflip */ text-align: right; } .navbox, .navbox-subgroup { background-color: #fdfdfd; /* Background color */ } .navbox-list { line-height: 1.5em; border-color: #fdfdfd; /* Must match background color */ } /* cell spacing for navbox cells */ tr + tr > .navbox-abovebelow, tr + tr > .navbox-group, tr + tr > .navbox-image, tr + tr > .navbox-list { /* Borders above 2nd, 3rd, etc. rows */ border-top: 2px solid #fdfdfd; /* Must match background color */ } .navbox th, .navbox-title { background-color: #ccccff; /* Level 1 color */ } .navbox-abovebelow, th.navbox-group, .navbox-subgroup .navbox-title { background-color: #ddddff; /* Level 2 color */ } .navbox-subgroup .navbox-group, .navbox-subgroup .navbox-abovebelow { background-color: #e6e6ff; /* Level 3 color */ } .navbox-even { background-color: #f7f7f7; /* Even row striping */ } .navbox-odd { background-color: transparent; /* Odd row striping */ } .navbox .hlist td dl, .navbox .hlist td ol, .navbox .hlist td ul, .navbox td.hlist dl, .navbox td.hlist ol, .navbox td.hlist ul { padding: 0.125em 0; /* Adjust hlist padding in navboxes */ } /* Default styling for Navbar template */ .navbar { display: inline; font-size: 88%; font-weight: normal; } .navbar ul { display: inline; white-space: nowrap; } .mw-body-content .navbar ul { line-height: inherit; } .navbar li { word-spacing: -0.125em; } .navbar.mini li abbr[title] { font-variant: small-caps; border-bottom: none; text-decoration: none; cursor: inherit; } /* Navbar styling when nested in infobox and navbox */ .infobox .navbar { font-size: 100%; } .navbox .navbar { display: block; font-size: 100%; } .navbox-title .navbar { /* @noflip */ float: left; /* @noflip */ text-align: left; /* @noflip */ margin-right: 0.5em; } .infobox { border: 1px solid #a2a9b1; border-spacing: 3px; background-color: #f8f9fa; color: black; /* @noflip */ margin: 0.5em 0 0.5em 1em; padding: 0.2em; /* @noflip */ float: right; /* @noflip */ clear: right; font-size: 88%; line-height: 1.5em; } .infobox caption { font-size: 125%; font-weight: bold; padding: 0.2em; text-align: center; } .infobox td, .infobox th { vertical-align: top; /* @noflip */ text-align: left; } .infobox.bordered { border-collapse: collapse; } .infobox.bordered td, .infobox.bordered th { border: 1px solid #a2a9b1; } .infobox.bordered .borderless td, .infobox.bordered .borderless th { border: 0; } .infobox.sisterproject { width: 20em; font-size: 90%; } .infobox.standard-talk { border: 1px solid #c0c090; background-color: #f8eaba; } .infobox.standard-talk.bordered td, .infobox.standard-talk.bordered th { border: 1px solid #c0c090; } /* styles for bordered infobox with merged rows */ .infobox.bordered .mergedtoprow td, .infobox.bordered .mergedtoprow th { border: 0; border-top: 1px solid #a2a9b1; /* @noflip */ border-right: 1px solid #a2a9b1; } .infobox.bordered .mergedrow td, .infobox.bordered .mergedrow th { border: 0; /* @noflip */ border-right: 1px solid #a2a9b1; } /* Styles for geography infoboxes, eg countries, country subdivisions, cities, etc. */ .infobox.geography { border-collapse: collapse; line-height: 1.2em; font-size: 90%; } .infobox.geography td, .infobox.geography th { border-top: 1px solid #a2a9b1; padding: 0.4em 0.6em 0.4em 0.6em; } .infobox.geography .mergedtoprow td, .infobox.geography .mergedtoprow th { border-top: 1px solid #a2a9b1; padding: 0.4em 0.6em 0.2em 0.6em; } .infobox.geography .mergedrow td, .infobox.geography .mergedrow th { border: 0; padding: 0 0.6em 0.2em 0.6em; } .infobox.geography .mergedbottomrow td, .infobox.geography .mergedbottomrow th { border-top: 0; border-bottom: 1px solid #a2a9b1; padding: 0 0.6em 0.4em 0.6em; } .infobox.geography .maptable td, .infobox.geography .maptable th { border: 0; padding: 0; } div.listenlist { background: url("//upload.wikimedia.org/wikipedia/commons/4/47/Sound-icon.svg") no-repeat scroll 0 0 transparent; background-size: 30px; padding-left: 40px; } /* Fix for hieroglyphs specificity issue in infoboxes ([[phab:T43869]]) */ table.mw-hiero-table td { vertical-align: middle; } /* Style rules for media list templates */ /* TemplateStyles */ div.medialist { min-height: 50px; margin: 1em; /* @noflip */ background-position: top left; background-repeat: no-repeat; } div.medialist ul { list-style-type: none; list-style-image: none; margin: 0; } div.medialist ul li { padding-bottom: 0.5em; } div.medialist ul li li { font-size: 91%; padding-bottom: 0; } /* Messagebox templates */ .messagebox { border: 1px solid #a2a9b1; background-color: #f8f9fa; width: 80%; margin: 0 auto 1em auto; padding: .2em; } .messagebox.merge { border: 1px solid #c0b8cc; background-color: #f0e5ff; text-align: center; } .messagebox.cleanup { border: 1px solid #9f9fff; background-color: #efefff; text-align: center; } .messagebox.standard-talk { border: 1px solid #c0c090; background-color: #f8eaba; margin: 4px auto; } /* For old WikiProject banners inside banner shells. */ .mbox-inside .standard-talk, .messagebox.nested-talk { border: 1px solid #c0c090; background-color: #f8eaba; width: 100%; margin: 2px 0; padding: 2px; } .messagebox.small { width: 238px; font-size: 85%; /* @noflip */ float: right; clear: both; /* @noflip */ margin: 0 0 1em 1em; line-height: 1.25em; } .messagebox.small-talk { width: 238px; font-size: 85%; /* @noflip */ float: right; clear: both; /* @noflip */ margin: 0 0 1em 1em; line-height: 1.25em; background-color: #F8EABA; } /* Cell sizes for ambox/tmbox/imbox/cmbox/ombox/fmbox/dmbox message boxes */ th.mbox-text, td.mbox-text { /* The message body cell(s) */ border: none; /* @noflip */ padding: 0.25em 0.9em; /* 0.9em left/right */ width: 100%; /* Make all mboxes the same width regardless of text length */ } td.mbox-image { /* The left image cell */ border: none; /* @noflip */ padding: 2px 0 2px 0.9em; /* 0.9em left, 0px right */ text-align: center; } td.mbox-imageright { /* The right image cell */ border: none; /* @noflip */ padding: 2px 0.9em 2px 0; /* 0px left, 0.9em right */ text-align: center; } td.mbox-empty-cell { /* An empty narrow cell */ border: none; padding: 0; width: 1px; } /* Article message box styles */ table.ambox { margin: 0 10%; /* 10% = Will not overlap with other elements */ border: 1px solid #a2a9b1; /* @noflip */ border-left: 10px solid #36c; /* Default "notice" blue */ background-color: #fbfbfb; box-sizing: border-box; } table.ambox + table.ambox { /* Single border between stacked boxes. */ margin-top: -1px; } .ambox th.mbox-text, .ambox td.mbox-text { /* The message body cell(s) */ padding: 0.25em 0.5em; /* 0.5em left/right */ } .ambox td.mbox-image { /* The left image cell */ /* @noflip */ padding: 2px 0 2px 0.5em; /* 0.5em left, 0px right */ } .ambox td.mbox-imageright { /* The right image cell */ /* @noflip */ padding: 2px 0.5em 2px 0; /* 0px left, 0.5em right */ } table.ambox-notice { /* @noflip */ border-left: 10px solid #36c; /* Blue */ } table.ambox-speedy { /* @noflip */ border-left: 10px solid #b32424; /* Red */ background-color: #fee7e6; /* Pink */ } table.ambox-delete { /* @noflip */ border-left: 10px solid #b32424; /* Red */ } table.ambox-content { /* @noflip */ border-left: 10px solid #f28500; /* Orange */ } table.ambox-style { /* @noflip */ border-left: 10px solid #fc3; /* Yellow */ } table.ambox-move { /* @noflip */ border-left: 10px solid #9932cc; /* Purple */ } table.ambox-protection { /* @noflip */ border-left: 10px solid #a2a9b1; /* Gray-gold */ } /* Image message box styles */ table.imbox { margin: 4px 10%; border-collapse: collapse; border: 3px solid #36c; /* Default "notice" blue */ background-color: #fbfbfb; box-sizing: border-box; } .imbox .mbox-text .imbox { /* For imboxes inside imbox-text cells. */ margin: 0 -0.5em; /* 0.9 - 0.5 = 0.4em left/right. */ display: block; /* Fix for webkit to force 100% width. */ } .mbox-inside .imbox { /* For imboxes inside other templates. */ margin: 4px; } table.imbox-notice { border: 3px solid #36c; /* Blue */ } table.imbox-speedy { border: 3px solid #b32424; /* Red */ background-color: #fee7e6; /* Pink */ } table.imbox-delete { border: 3px solid #b32424; /* Red */ } table.imbox-content { border: 3px solid #f28500; /* Orange */ } table.imbox-style { border: 3px solid #fc3; /* Yellow */ } table.imbox-move { border: 3px solid #9932cc; /* Purple */ } table.imbox-protection { border: 3px solid #a2a9b1; /* Gray-gold */ } table.imbox-license { border: 3px solid #88a; /* Dark gray */ background-color: #f7f8ff; /* Light gray */ } table.imbox-featured { border: 3px solid #cba135; /* Brown-gold */ } /* Category message box styles */ table.cmbox { margin: 3px 10%; border-collapse: collapse; border: 1px solid #a2a9b1; background-color: #DFE8FF; /* Default "notice" blue */ box-sizing: border-box; } table.cmbox-notice { background-color: #D8E8FF; /* Blue */ } table.cmbox-speedy { margin-top: 4px; margin-bottom: 4px; border: 4px solid #b32424; /* Red */ background-color: #FFDBDB; /* Pink */ } table.cmbox-delete { background-color: #FFDBDB; /* Red */ } table.cmbox-content { background-color: #FFE7CE; /* Orange */ } table.cmbox-style { background-color: #FFF9DB; /* Yellow */ } table.cmbox-move { background-color: #E4D8FF; /* Purple */ } table.cmbox-protection { background-color: #EFEFE1; /* Gray-gold */ } /* Other pages message box styles */ table.ombox { margin: 4px 10%; border-collapse: collapse; border: 1px solid #a2a9b1; /* Default "notice" gray */ background-color: #f8f9fa; box-sizing: border-box; } table.ombox-notice { border: 1px solid #a2a9b1; /* Gray */ } table.ombox-speedy { border: 2px solid #b32424; /* Red */ background-color: #fee7e6; /* Pink */ } table.ombox-delete { border: 2px solid #b32424; /* Red */ } table.ombox-content { border: 1px solid #f28500; /* Orange */ } table.ombox-style { border: 1px solid #fc3; /* Yellow */ } table.ombox-move { border: 1px solid #9932cc; /* Purple */ } table.ombox-protection { border: 2px solid #a2a9b1; /* Gray-gold */ } /* Talk page message box styles */ table.tmbox { margin: 4px 10%; border-collapse: collapse; border: 1px solid #c0c090; /* Default "notice" gray-brown */ background-color: #f8eaba; min-width: 80%; box-sizing: border-box; } .tmbox.mbox-small { min-width: 0; /* reset the min-width of tmbox above */ } .mediawiki .mbox-inside .tmbox { /* For tmboxes inside other templates. The "mediawiki" class ensures that */ margin: 2px 0; /* this declaration overrides other styles (including mbox-small above) */ width: 100%; /* For Safari and Opera */ } .mbox-inside .tmbox.mbox-small { /* "small" tmboxes should not be small when */ line-height: 1.5em; /* also "nested", so reset styles that are */ font-size: 100%; /* set in "mbox-small" above. */ } table.tmbox-speedy { border: 2px solid #b32424; /* Red */ background-color: #fee7e6; /* Pink */ } table.tmbox-delete { border: 2px solid #b32424; /* Red */ } table.tmbox-content { border: 2px solid #f28500; /* Orange */ } table.tmbox-style { border: 2px solid #fc3; /* Yellow */ } table.tmbox-move { border: 2px solid #9932cc; /* Purple */ } table.tmbox-protection, table.tmbox-notice { border: 1px solid #c0c090; /* Gray-brown */ } /* Disambig and set index box styles */ table.dmbox { clear: both; margin: 0.9em 1em; border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; background-color: transparent; } /* Footer and header message box styles */ table.fmbox { clear: both; margin: 0.2em 0; width: 100%; border: 1px solid #a2a9b1; background-color: #f8f9fa; /* Default "system" gray */ box-sizing: border-box; } table.fmbox-system { background-color: #f8f9fa; } table.fmbox-warning { border: 1px solid #bb7070; /* Dark pink */ background-color: #ffdbdb; /* Pink */ } table.fmbox-editnotice { background-color: transparent; } /* Div based "warning" style fmbox messages. */ div.mw-warning-with-logexcerpt, div.mw-lag-warn-high, div.mw-cascadeprotectedwarning, div#mw-protect-cascadeon, div.titleblacklist-warning, div.locked-warning { clear: both; margin: 0.2em 0; border: 1px solid #bb7070; background-color: #ffdbdb; padding: 0.25em 0.9em; box-sizing: border-box; } /* These mbox-small classes must be placed after all other ambox/tmbox/ombox etc classes. "html body.mediawiki" is so they override "table.ambox + table.ambox" above. */ html body.mediawiki .mbox-small { /* For the "small=yes" option. */ /* @noflip */ clear: right; /* @noflip */ float: right; /* @noflip */ margin: 4px 0 4px 1em; box-sizing: border-box; width: 238px; font-size: 88%; line-height: 1.25em; } html body.mediawiki .mbox-small-left { /* For the "small=left" option. */ /* @noflip */ margin: 4px 1em 4px 0; box-sizing: border-box; overflow: hidden; width: 238px; border-collapse: collapse; font-size: 88%; line-height: 1.25em; } /* Style for compact ambox */ /* Hide the images */ .compact-ambox table .mbox-image, .compact-ambox table .mbox-imageright, .compact-ambox table .mbox-empty-cell { display: none; } /* Remove borders, backgrounds, padding, etc. */ .compact-ambox table.ambox { border: none; border-collapse: collapse; background-color: transparent; margin: 0 0 0 1.6em !important; padding: 0 !important; width: auto; display: block; } body.mediawiki .compact-ambox table.mbox-small-left { font-size: 100%; width: auto; margin: 0; } /* Style the text cell as a list item and remove its padding */ .compact-ambox table .mbox-text { padding: 0 !important; margin: 0 !important; } .compact-ambox table .mbox-text-span { display: list-item; line-height: 1.5em; list-style-type: square; list-style-image: url(/w/skins/MonoBook/resources/images/bullet.gif); } .skin-vector .compact-ambox table .mbox-text-span { list-style-type: disc; list-style-image: url(/w/skins/Vector/images/bullet-icon.svg); list-style-image: url(/w/skins/Vector/images/bullet-icon.png)\9; } /* Allow for hiding text in compact form */ .compact-ambox .hide-when-compact { display: none; } /* For template documentation */ /* TemplateStyles */ .template-documentation { clear: both; margin: 1em 0 0 0; border: 1px solid #a2a9b1; background-color: #ecfcf4; padding: 1em; } d9e4c7221dafae29c01080b36effd079723d287b Template:Infobox 10 17 28 27 2023-08-27T19:18:34Z Alxira5 4 1 revision imported: Templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki {{#invoke:Infobox|infobox}}<noinclude> {{documentation}} </noinclude> 627ee6fcf4d4f108fe054b5c476201cad0ed7717 Template:Documentation 10 18 30 29 2023-08-27T19:18:35Z Alxira5 4 1 revision imported: Templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki {{#invoke:documentation|main|_content={{ {{#invoke:documentation|contentTitle}}}}}}<noinclude>[[Category:Templates]]</noinclude> 9885bb4fa99bf3d5b960e73606bbb8eed3026877 Module:Infobox 828 19 32 31 2023-08-27T19:26:21Z Alxira5 4 1 revision imported: Modules imported from Miraheze Developers, published under the CC-BY-SA 4.0 license Scribunto text/plain -- -- This module implements {{Infobox}} -- local p = {} local args = {} local origArgs = {} local root local function notempty( s ) return s and s:match( '%S' ) end local function fixChildBoxes(sval, tt) if notempty(sval) then local marker = '<span class=special_infobox_marker>' local s = sval s = mw.ustring.gsub(s, '(<%s*[Tt][Rr])', marker .. '%1') s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>)', '%1' .. marker) if s:match(marker) then s = mw.ustring.gsub(s, marker .. '%s*' .. marker, '') s = mw.ustring.gsub(s, '([\r\n]|-[^\r\n]*[\r\n])%s*' .. marker, '%1') s = mw.ustring.gsub(s, marker .. '%s*([\r\n]|-)', '%1') s = mw.ustring.gsub(s, '(</[Cc][Aa][Pp][Tt][Ii][Oo][Nn]%s*>%s*)' .. marker, '%1') s = mw.ustring.gsub(s, '(<%s*[Tt][Aa][Bb][Ll][Ee][^<>]*>%s*)' .. marker, '%1') s = mw.ustring.gsub(s, '^(%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1') s = mw.ustring.gsub(s, '([\r\n]%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1') s = mw.ustring.gsub(s, marker .. '(%s*</[Tt][Aa][Bb][Ll][Ee]%s*>)', '%1') s = mw.ustring.gsub(s, marker .. '(%s*\n|%})', '%1') end if s:match(marker) then local subcells = mw.text.split(s, marker) s = '' for k = 1, #subcells do if k == 1 then s = s .. subcells[k] .. '</' .. tt .. '></tr>' elseif k == #subcells then local rowstyle = ' style="display:none"' if notempty(subcells[k]) then rowstyle = '' end s = s .. '<tr' .. rowstyle ..'><' .. tt .. ' colspan=2>\n' .. subcells[k] elseif notempty(subcells[k]) then if (k % 2) == 0 then s = s .. subcells[k] else s = s .. '<tr><' .. tt .. ' colspan=2>\n' .. subcells[k] .. '</' .. tt .. '></tr>' end end end end -- the next two lines add a newline at the end of lists for the PHP parser -- https://en.wikipedia.org/w/index.php?title=Template_talk:Infobox_musical_artist&oldid=849054481 -- remove when [[:phab:T191516]] is fixed or OBE s = mw.ustring.gsub(s, '([\r\n][%*#;:][^\r\n]*)$', '%1\n') s = mw.ustring.gsub(s, '^([%*#;:][^\r\n]*)$', '%1\n') s = mw.ustring.gsub(s, '^([%*#;:])', '\n%1') s = mw.ustring.gsub(s, '^(%{%|)', '\n%1') return s else return sval end end local function union(t1, t2) -- Returns the union of the values of two tables, as a sequence. local vals = {} for k, v in pairs(t1) do vals[v] = true end for k, v in pairs(t2) do vals[v] = true end local ret = {} for k, v in pairs(vals) do table.insert(ret, k) end return ret end local function getArgNums(prefix) -- Returns a table containing the numbers of the arguments that exist -- for the specified prefix. For example, if the prefix was 'data', and -- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}. local nums = {} for k, v in pairs(args) do local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$') if num then table.insert(nums, tonumber(num)) end end table.sort(nums) return nums end local function addRow(rowArgs) -- Adds a row to the infobox, with either a header cell -- or a label/data cell combination. if rowArgs.header and rowArgs.header ~= '_BLANK_' then root :tag('tr') :addClass(rowArgs.rowclass) :cssText(rowArgs.rowstyle) :attr('id', rowArgs.rowid) :tag('th') :attr('colspan', 2) :attr('id', rowArgs.headerid) :addClass(rowArgs.class) :addClass(args.headerclass) :css('text-align', 'center') :cssText(args.headerstyle) :cssText(rowArgs.rowcellstyle) :wikitext(fixChildBoxes(rowArgs.header, 'th')) elseif rowArgs.data then if not rowArgs.data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then rowArgs.rowstyle = 'display:none' end local row = root:tag('tr') row:addClass(rowArgs.rowclass) row:cssText(rowArgs.rowstyle) row:attr('id', rowArgs.rowid) if rowArgs.label then row :tag('th') :attr('scope', 'row') :attr('id', rowArgs.labelid) :cssText(args.labelstyle) :cssText(rowArgs.rowcellstyle) :wikitext(rowArgs.label) :done() end local dataCell = row:tag('td') if not rowArgs.label then dataCell :attr('colspan', 2) :css('text-align', 'center') end dataCell :attr('id', rowArgs.dataid) :addClass(rowArgs.class) :cssText(rowArgs.datastyle) :cssText(rowArgs.rowcellstyle) :wikitext(fixChildBoxes(rowArgs.data, 'td')) end end local function renderTitle() if not args.title then return end root :tag('caption') :addClass(args.titleclass) :cssText(args.titlestyle) :wikitext(args.title) end local function renderAboveRow() if not args.above then return end root :tag('tr') :tag('th') :attr('colspan', 2) :addClass(args.aboveclass) :css('text-align', 'center') :css('font-size', '125%') :css('font-weight', 'bold') :cssText(args.abovestyle) :wikitext(fixChildBoxes(args.above,'th')) end local function renderBelowRow() if not args.below then return end root :tag('tr') :tag('td') :attr('colspan', '2') :addClass(args.belowclass) :css('text-align', 'center') :cssText(args.belowstyle) :wikitext(fixChildBoxes(args.below,'td')) end local function renderSubheaders() if args.subheader then args.subheader1 = args.subheader end if args.subheaderrowclass then args.subheaderrowclass1 = args.subheaderrowclass end local subheadernums = getArgNums('subheader') for k, num in ipairs(subheadernums) do addRow({ data = args['subheader' .. tostring(num)], datastyle = args.subheaderstyle, rowcellstyle = args['subheaderstyle' .. tostring(num)], class = args.subheaderclass, rowclass = args['subheaderrowclass' .. tostring(num)] }) end end local function renderImages() if args.image then args.image1 = args.image end if args.caption then args.caption1 = args.caption end local imagenums = getArgNums('image') for k, num in ipairs(imagenums) do local caption = args['caption' .. tostring(num)] local data = mw.html.create():wikitext(args['image' .. tostring(num)]) if caption then data :tag('div') :cssText(args.captionstyle) :wikitext(caption) end addRow({ data = tostring(data), datastyle = args.imagestyle, class = args.imageclass, rowclass = args['imagerowclass' .. tostring(num)] }) end end local function preprocessRows() -- Gets the union of the header and data argument numbers, -- and renders them all in order using addRow. local rownums = union(getArgNums('header'), getArgNums('data')) table.sort(rownums) local lastheader for k, num in ipairs(rownums) do if args['header' .. tostring(num)] then if lastheader then args['header' .. tostring(lastheader)] = nil end lastheader = num elseif args['data' .. tostring(num)] and args['data' .. tostring(num)]:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('^%S') then local data = args['data' .. tostring(num)] if data:gsub('%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]', ''):match('%S') then lastheader = nil end end end if lastheader then args['header' .. tostring(lastheader)] = nil end end local function renderRows() -- Gets the union of the header and data argument numbers, -- and renders them all in order using addRow. local rownums = union(getArgNums('header'), getArgNums('data')) table.sort(rownums) for k, num in ipairs(rownums) do addRow({ header = args['header' .. tostring(num)], label = args['label' .. tostring(num)], data = args['data' .. tostring(num)], datastyle = args.datastyle, class = args['class' .. tostring(num)], rowclass = args['rowclass' .. tostring(num)], rowstyle = args['rowstyle' .. tostring(num)], rowcellstyle = args['rowcellstyle' .. tostring(num)], dataid = args['dataid' .. tostring(num)], labelid = args['labelid' .. tostring(num)], headerid = args['headerid' .. tostring(num)], rowid = args['rowid' .. tostring(num)] }) end end local function renderItalicTitle() local italicTitle = args['italic title'] and mw.ustring.lower(args['italic title']) if italicTitle == '' or italicTitle == 'force' or italicTitle == 'yes' then root:wikitext(mw.getCurrentFrame():expandTemplate({title = 'italic title'})) end end local function _infobox() -- Specify the overall layout of the infobox, with special settings -- if the infobox is used as a 'child' inside another infobox. if args.child ~= 'yes' then root = mw.html.create('table') root :addClass((args.subbox ~= 'yes') and 'infobox' or nil) :addClass(args.bodyclass) if args.subbox == 'yes' then root :css('padding', '0') :css('border', 'none') :css('margin', '-3px') :css('width', 'auto') :css('min-width', '100%') :css('font-size', '100%') :css('clear', 'none') :css('float', 'none') :css('background-color', 'transparent') else root :css('width', '22em') end root :cssText(args.bodystyle) renderTitle() renderAboveRow() else root = mw.html.create() root :wikitext(args.title) end renderSubheaders() renderImages() if args.autoheaders then preprocessRows() end renderRows() renderBelowRow() renderItalicTitle() return tostring(root) end local function preprocessSingleArg(argName) -- If the argument exists and isn't blank, add it to the argument table. -- Blank arguments are treated as nil to match the behaviour of ParserFunctions. if origArgs[argName] and origArgs[argName] ~= '' then args[argName] = origArgs[argName] end end local function preprocessArgs(prefixTable, step) -- Assign the parameters with the given prefixes to the args table, in order, in batches -- of the step size specified. This is to prevent references etc. from appearing in the -- wrong order. The prefixTable should be an array containing tables, each of which has -- two possible fields, a "prefix" string and a "depend" table. The function always parses -- parameters containing the "prefix" string, but only parses parameters in the "depend" -- table if the prefix parameter is present and non-blank. if type(prefixTable) ~= 'table' then error("Non-table value detected for the prefix table", 2) end if type(step) ~= 'number' then error("Invalid step value detected", 2) end -- Get arguments without a number suffix, and check for bad input. for i,v in ipairs(prefixTable) do if type(v) ~= 'table' or type(v.prefix) ~= "string" or (v.depend and type(v.depend) ~= 'table') then error('Invalid input detected to preprocessArgs prefix table', 2) end preprocessSingleArg(v.prefix) -- Only parse the depend parameter if the prefix parameter is present and not blank. if args[v.prefix] and v.depend then for j, dependValue in ipairs(v.depend) do if type(dependValue) ~= 'string' then error('Invalid "depend" parameter value detected in preprocessArgs') end preprocessSingleArg(dependValue) end end end -- Get arguments with number suffixes. local a = 1 -- Counter variable. local moreArgumentsExist = true while moreArgumentsExist == true do moreArgumentsExist = false for i = a, a + step - 1 do for j,v in ipairs(prefixTable) do local prefixArgName = v.prefix .. tostring(i) if origArgs[prefixArgName] then moreArgumentsExist = true -- Do another loop if any arguments are found, even blank ones. preprocessSingleArg(prefixArgName) end -- Process the depend table if the prefix argument is present and not blank, or -- we are processing "prefix1" and "prefix" is present and not blank, and -- if the depend table is present. if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then for j,dependValue in ipairs(v.depend) do local dependArgName = dependValue .. tostring(i) preprocessSingleArg(dependArgName) end end end end a = a + step end end local function parseDataParameters() -- Parse the data parameters in the same order that the old {{infobox}} did, so that -- references etc. will display in the expected places. Parameters that depend on -- another parameter are only processed if that parameter is present, to avoid -- phantom references appearing in article reference lists. preprocessSingleArg('autoheaders') preprocessSingleArg('child') preprocessSingleArg('bodyclass') preprocessSingleArg('subbox') preprocessSingleArg('bodystyle') preprocessSingleArg('title') preprocessSingleArg('titleclass') preprocessSingleArg('titlestyle') preprocessSingleArg('above') preprocessSingleArg('aboveclass') preprocessSingleArg('abovestyle') preprocessArgs({ {prefix = 'subheader', depend = {'subheaderstyle', 'subheaderrowclass'}} }, 10) preprocessSingleArg('subheaderstyle') preprocessSingleArg('subheaderclass') preprocessArgs({ {prefix = 'image', depend = {'caption', 'imagerowclass'}} }, 10) preprocessSingleArg('captionstyle') preprocessSingleArg('imagestyle') preprocessSingleArg('imageclass') preprocessArgs({ {prefix = 'header'}, {prefix = 'data', depend = {'label'}}, {prefix = 'rowclass'}, {prefix = 'rowstyle'}, {prefix = 'rowcellstyle'}, {prefix = 'class'}, {prefix = 'dataid'}, {prefix = 'labelid'}, {prefix = 'headerid'}, {prefix = 'rowid'} }, 50) preprocessSingleArg('headerclass') preprocessSingleArg('headerstyle') preprocessSingleArg('labelstyle') preprocessSingleArg('datastyle') preprocessSingleArg('below') preprocessSingleArg('belowclass') preprocessSingleArg('belowstyle') preprocessSingleArg('name') args['italic title'] = origArgs['italic title'] -- different behaviour if blank or absent preprocessSingleArg('decat') end function p.infobox(frame) -- If called via #invoke, use the args passed into the invoking template. -- Otherwise, for testing purposes, assume args are being passed directly in. if frame == mw.getCurrentFrame() then origArgs = frame:getParent().args else origArgs = frame end parseDataParameters() return _infobox() end function p.infoboxTemplate(frame) -- For calling via #invoke within a template origArgs = {} for k,v in pairs(frame.args) do origArgs[k] = mw.text.trim(v) end parseDataParameters() return _infobox() end return p c6ac51f9e2faf9c2f3aba1fb8c05af98db47f4d4 Module:Documentation 828 20 34 33 2023-08-27T19:26:21Z Alxira5 4 1 revision imported: Modules imported from Miraheze Developers, published under the CC-BY-SA 4.0 license Scribunto text/plain -- This module implements {{documentation}}. -- Get required modules. local getArgs = require('Module:Arguments').getArgs -- Get the config table. local cfg = mw.loadData('Module:Documentation/config') local p = {} -- Often-used functions. local ugsub = mw.ustring.gsub ---------------------------------------------------------------------------- -- Helper functions -- -- These are defined as local functions, but are made available in the p -- table for testing purposes. ---------------------------------------------------------------------------- local function message(cfgKey, valArray, expectType) --[[ -- Gets a message from the cfg table and formats it if appropriate. -- The function raises an error if the value from the cfg table is not -- of the type expectType. The default type for expectType is 'string'. -- If the table valArray is present, strings such as $1, $2 etc. in the -- message are substituted with values from the table keys [1], [2] etc. -- For example, if the message "foo-message" had the value 'Foo $2 bar $1.', -- message('foo-message', {'baz', 'qux'}) would return "Foo qux bar baz." --]] local msg = cfg[cfgKey] expectType = expectType or 'string' if type(msg) ~= expectType then error('message: type error in message cfg.' .. cfgKey .. ' (' .. expectType .. ' expected, got ' .. type(msg) .. ')', 2) end if not valArray then return msg end local function getMessageVal(match) match = tonumber(match) return valArray[match] or error('message: no value found for key $' .. match .. ' in message cfg.' .. cfgKey, 4) end return ugsub(msg, '$([1-9][0-9]*)', getMessageVal) end p.message = message local function makeWikilink(page, display) if display then return mw.ustring.format('[[%s|%s]]', page, display) else return mw.ustring.format('[[%s]]', page) end end p.makeWikilink = makeWikilink local function makeCategoryLink(cat, sort) local catns = mw.site.namespaces[14].name return makeWikilink(catns .. ':' .. cat, sort) end p.makeCategoryLink = makeCategoryLink local function makeUrlLink(url, display) return mw.ustring.format('[%s %s]', url, display) end p.makeUrlLink = makeUrlLink local function makeToolbar(...) local ret = {} local lim = select('#', ...) if lim < 1 then return nil end for i = 1, lim do ret[#ret + 1] = select(i, ...) end -- 'documentation-toolbar' return '<span class="' .. message('toolbar-class') .. '">(' .. table.concat(ret, ' &#124; ') .. ')</span>' end p.makeToolbar = makeToolbar ---------------------------------------------------------------------------- -- Argument processing ---------------------------------------------------------------------------- local function makeInvokeFunc(funcName) return function (frame) local args = getArgs(frame, { valueFunc = function (key, value) if type(value) == 'string' then value = value:match('^%s*(.-)%s*$') -- Remove whitespace. if key == 'heading' or value ~= '' then return value else return nil end else return value end end }) return p[funcName](args) end end ---------------------------------------------------------------------------- -- Entry points ---------------------------------------------------------------------------- function p.nonexistent(frame) if mw.title.getCurrentTitle().subpageText == 'testcases' then return frame:expandTemplate{title = 'module test cases notice'} else return p.main(frame) end end p.main = makeInvokeFunc('_main') function p._main(args) --[[ -- This function defines logic flow for the module. -- @args - table of arguments passed by the user --]] local env = p.getEnvironment(args) local root = mw.html.create() root :tag('div') -- 'documentation-container' :addClass(message('container')) :attr('role', 'complementary') :attr('aria-labelledby', args.heading ~= '' and 'documentation-heading' or nil) :attr('aria-label', args.heading == '' and 'Documentation' or nil) :newline() :tag('div') -- 'documentation' :addClass(message('main-div-classes')) :newline() :wikitext(p._startBox(args, env)) :wikitext(p._content(args, env)) :tag('div') -- 'documentation-clear' :addClass(message('clear')) :done() :newline() :done() :wikitext(p._endBox(args, env)) :done() :wikitext(p.addTrackingCategories(env)) -- 'Module:Documentation/styles.css' return mw.getCurrentFrame():extensionTag ( 'templatestyles', '', {src=cfg['templatestyles'] }) .. tostring(root) end ---------------------------------------------------------------------------- -- Environment settings ---------------------------------------------------------------------------- function p.getEnvironment(args) --[[ -- Returns a table with information about the environment, including title -- objects and other namespace- or path-related data. -- @args - table of arguments passed by the user -- -- Title objects include: -- env.title - the page we are making documentation for (usually the current title) -- env.templateTitle - the template (or module, file, etc.) -- env.docTitle - the /doc subpage. -- env.sandboxTitle - the /sandbox subpage. -- env.testcasesTitle - the /testcases subpage. -- -- Data includes: -- env.subjectSpace - the number of the title's subject namespace. -- env.docSpace - the number of the namespace the title puts its documentation in. -- env.docpageBase - the text of the base page of the /doc, /sandbox and /testcases pages, with namespace. -- env.compareUrl - URL of the Special:ComparePages page comparing the sandbox with the template. -- -- All table lookups are passed through pcall so that errors are caught. If an error occurs, the value -- returned will be nil. --]] local env, envFuncs = {}, {} -- Set up the metatable. If triggered we call the corresponding function in the envFuncs table. The value -- returned by that function is memoized in the env table so that we don't call any of the functions -- more than once. (Nils won't be memoized.) setmetatable(env, { __index = function (t, key) local envFunc = envFuncs[key] if envFunc then local success, val = pcall(envFunc) if success then env[key] = val -- Memoise the value. return val end end return nil end }) function envFuncs.title() -- The title object for the current page, or a test page passed with args.page. local title local titleArg = args.page if titleArg then title = mw.title.new(titleArg) else title = mw.title.getCurrentTitle() end return title end function envFuncs.templateTitle() --[[ -- The template (or module, etc.) title object. -- Messages: -- 'sandbox-subpage' --> 'sandbox' -- 'testcases-subpage' --> 'testcases' --]] local subjectSpace = env.subjectSpace local title = env.title local subpage = title.subpageText if subpage == message('sandbox-subpage') or subpage == message('testcases-subpage') then return mw.title.makeTitle(subjectSpace, title.baseText) else return mw.title.makeTitle(subjectSpace, title.text) end end function envFuncs.docTitle() --[[ -- Title object of the /doc subpage. -- Messages: -- 'doc-subpage' --> 'doc' --]] local title = env.title local docname = args[1] -- User-specified doc page. local docpage if docname then docpage = docname else docpage = env.docpageBase .. '/' .. message('doc-subpage') end return mw.title.new(docpage) end function envFuncs.sandboxTitle() --[[ -- Title object for the /sandbox subpage. -- Messages: -- 'sandbox-subpage' --> 'sandbox' --]] return mw.title.new(env.docpageBase .. '/' .. message('sandbox-subpage')) end function envFuncs.testcasesTitle() --[[ -- Title object for the /testcases subpage. -- Messages: -- 'testcases-subpage' --> 'testcases' --]] return mw.title.new(env.docpageBase .. '/' .. message('testcases-subpage')) end function envFuncs.subjectSpace() -- The subject namespace number. return mw.site.namespaces[env.title.namespace].subject.id end function envFuncs.docSpace() -- The documentation namespace number. For most namespaces this is the -- same as the subject namespace. However, pages in the Article, File, -- MediaWiki or Category namespaces must have their /doc, /sandbox and -- /testcases pages in talk space. local subjectSpace = env.subjectSpace if subjectSpace == 0 or subjectSpace == 6 or subjectSpace == 8 or subjectSpace == 14 then return subjectSpace + 1 else return subjectSpace end end function envFuncs.docpageBase() -- The base page of the /doc, /sandbox, and /testcases subpages. -- For some namespaces this is the talk page, rather than the template page. local templateTitle = env.templateTitle local docSpace = env.docSpace local docSpaceText = mw.site.namespaces[docSpace].name -- Assemble the link. docSpace is never the main namespace, so we can hardcode the colon. return docSpaceText .. ':' .. templateTitle.text end function envFuncs.compareUrl() -- Diff link between the sandbox and the main template using [[Special:ComparePages]]. local templateTitle = env.templateTitle local sandboxTitle = env.sandboxTitle if templateTitle.exists and sandboxTitle.exists then local compareUrl = mw.uri.fullUrl( 'Special:ComparePages', { page1 = templateTitle.prefixedText, page2 = sandboxTitle.prefixedText} ) return tostring(compareUrl) else return nil end end return env end ---------------------------------------------------------------------------- -- Start box ---------------------------------------------------------------------------- p.startBox = makeInvokeFunc('_startBox') function p._startBox(args, env) --[[ -- This function generates the start box. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- The actual work is done by p.makeStartBoxLinksData and p.renderStartBoxLinks which make -- the [view] [edit] [history] [purge] links, and by p.makeStartBoxData and p.renderStartBox -- which generate the box HTML. --]] env = env or p.getEnvironment(args) local links local content = args.content if not content or args[1] then -- No need to include the links if the documentation is on the template page itself. local linksData = p.makeStartBoxLinksData(args, env) if linksData then links = p.renderStartBoxLinks(linksData) end end -- Generate the start box html. local data = p.makeStartBoxData(args, env, links) if data then return p.renderStartBox(data) else -- User specified no heading. return nil end end function p.makeStartBoxLinksData(args, env) --[[ -- Does initial processing of data to make the [view] [edit] [history] [purge] links. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- Messages: -- 'view-link-display' --> 'view' -- 'edit-link-display' --> 'edit' -- 'history-link-display' --> 'history' -- 'purge-link-display' --> 'purge' -- 'module-preload' --> 'Template:Documentation/preload-module-doc' -- 'docpage-preload' --> 'Template:Documentation/preload' -- 'create-link-display' --> 'create' --]] local subjectSpace = env.subjectSpace local title = env.title local docTitle = env.docTitle if not title or not docTitle then return nil end if docTitle.isRedirect then docTitle = docTitle.redirectTarget end local data = {} data.title = title data.docTitle = docTitle -- View, display, edit, and purge links if /doc exists. data.viewLinkDisplay = message('view-link-display') data.editLinkDisplay = message('edit-link-display') data.historyLinkDisplay = message('history-link-display') data.purgeLinkDisplay = message('purge-link-display') -- Create link if /doc doesn't exist. local preload = args.preload if not preload then if subjectSpace == 828 then -- Module namespace preload = message('module-preload') else preload = message('docpage-preload') end end data.preload = preload data.createLinkDisplay = message('create-link-display') return data end function p.renderStartBoxLinks(data) --[[ -- Generates the [view][edit][history][purge] or [create][purge] links from the data table. -- @data - a table of data generated by p.makeStartBoxLinksData --]] local function escapeBrackets(s) -- Escapes square brackets with HTML entities. s = s:gsub('%[', '&#91;') -- Replace square brackets with HTML entities. s = s:gsub('%]', '&#93;') return s end local ret local docTitle = data.docTitle local title = data.title local purgeLink = makeUrlLink(title:fullUrl{action = 'purge'}, data.purgeLinkDisplay) if docTitle.exists then local viewLink = makeWikilink(docTitle.prefixedText, data.viewLinkDisplay) local editLink = makeUrlLink(docTitle:fullUrl{action = 'edit'}, data.editLinkDisplay) local historyLink = makeUrlLink(docTitle:fullUrl{action = 'history'}, data.historyLinkDisplay) ret = '[%s] [%s] [%s] [%s]' ret = escapeBrackets(ret) ret = mw.ustring.format(ret, viewLink, editLink, historyLink, purgeLink) else local createLink = makeUrlLink(docTitle:fullUrl{action = 'edit', preload = data.preload}, data.createLinkDisplay) ret = '[%s] [%s]' ret = escapeBrackets(ret) ret = mw.ustring.format(ret, createLink, purgeLink) end return ret end function p.makeStartBoxData(args, env, links) --[=[ -- Does initial processing of data to pass to the start-box render function, p.renderStartBox. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- @links - a string containing the [view][edit][history][purge] links - could be nil if there's an error. -- -- Messages: -- 'documentation-icon-wikitext' --> '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]' -- 'template-namespace-heading' --> 'Template documentation' -- 'module-namespace-heading' --> 'Module documentation' -- 'file-namespace-heading' --> 'Summary' -- 'other-namespaces-heading' --> 'Documentation' -- 'testcases-create-link-display' --> 'create' --]=] local subjectSpace = env.subjectSpace if not subjectSpace then -- Default to an "other namespaces" namespace, so that we get at least some output -- if an error occurs. subjectSpace = 2 end local data = {} -- Heading local heading = args.heading -- Blank values are not removed. if heading == '' then -- Don't display the start box if the heading arg is defined but blank. return nil end if heading then data.heading = heading elseif subjectSpace == 10 then -- Template namespace data.heading = message('documentation-icon-wikitext') .. ' ' .. message('template-namespace-heading') elseif subjectSpace == 828 then -- Module namespace data.heading = message('documentation-icon-wikitext') .. ' ' .. message('module-namespace-heading') elseif subjectSpace == 6 then -- File namespace data.heading = message('file-namespace-heading') else data.heading = message('other-namespaces-heading') end -- Heading CSS local headingStyle = args['heading-style'] if headingStyle then data.headingStyleText = headingStyle else -- 'documentation-heading' data.headingClass = message('main-div-heading-class') end -- Data for the [view][edit][history][purge] or [create] links. if links then -- 'mw-editsection-like plainlinks' data.linksClass = message('start-box-link-classes') data.links = links end return data end function p.renderStartBox(data) -- Renders the start box html. -- @data - a table of data generated by p.makeStartBoxData. local sbox = mw.html.create('div') sbox -- 'documentation-startbox' :addClass(message('start-box-class')) :newline() :tag('span') :addClass(data.headingClass) :attr('id', 'documentation-heading') :cssText(data.headingStyleText) :wikitext(data.heading) local links = data.links if links then sbox:tag('span') :addClass(data.linksClass) :attr('id', data.linksId) :wikitext(links) end return tostring(sbox) end ---------------------------------------------------------------------------- -- Documentation content ---------------------------------------------------------------------------- p.content = makeInvokeFunc('_content') function p._content(args, env) -- Displays the documentation contents -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment env = env or p.getEnvironment(args) local docTitle = env.docTitle local content = args.content if not content and docTitle and docTitle.exists then content = args._content or mw.getCurrentFrame():expandTemplate{title = docTitle.prefixedText} end -- The line breaks below are necessary so that "=== Headings ===" at the start and end -- of docs are interpreted correctly. return '\n' .. (content or '') .. '\n' end p.contentTitle = makeInvokeFunc('_contentTitle') function p._contentTitle(args, env) env = env or p.getEnvironment(args) local docTitle = env.docTitle if not args.content and docTitle and docTitle.exists then return docTitle.prefixedText else return '' end end ---------------------------------------------------------------------------- -- End box ---------------------------------------------------------------------------- p.endBox = makeInvokeFunc('_endBox') function p._endBox(args, env) --[=[ -- This function generates the end box (also known as the link box). -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- --]=] -- Get environment data. env = env or p.getEnvironment(args) local subjectSpace = env.subjectSpace local docTitle = env.docTitle if not subjectSpace or not docTitle then return nil end -- Check whether we should output the end box at all. Add the end -- box by default if the documentation exists or if we are in the -- user, module or template namespaces. local linkBox = args['link box'] if linkBox == 'off' or not ( docTitle.exists or subjectSpace == 2 or subjectSpace == 828 or subjectSpace == 10 ) then return nil end -- Assemble the link box. local text = '' if linkBox then text = text .. linkBox else text = text .. (p.makeDocPageBlurb(args, env) or '') -- "This documentation is transcluded from [[Foo]]." if subjectSpace == 2 or subjectSpace == 10 or subjectSpace == 828 then -- We are in the user, template or module namespaces. -- Add sandbox and testcases links. -- "Editors can experiment in this template's sandbox and testcases pages." text = text .. (p.makeExperimentBlurb(args, env) or '') .. '<br />' if not args.content and not args[1] then -- "Please add categories to the /doc subpage." -- Don't show this message with inline docs or with an explicitly specified doc page, -- as then it is unclear where to add the categories. text = text .. (p.makeCategoriesBlurb(args, env) or '') end text = text .. ' ' .. (p.makeSubpagesBlurb(args, env) or '') --"Subpages of this template" end end local box = mw.html.create('div') -- 'documentation-metadata' box:attr('role', 'note') :addClass(message('end-box-class')) -- 'plainlinks' :addClass(message('end-box-plainlinks')) :wikitext(text) :done() return '\n' .. tostring(box) end function p.makeDocPageBlurb(args, env) --[=[ -- Makes the blurb "This documentation is transcluded from [[Template:Foo]] (edit, history)". -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- Messages: -- 'edit-link-display' --> 'edit' -- 'history-link-display' --> 'history' -- 'transcluded-from-blurb' --> -- 'The above [[Wikipedia:Template documentation|documentation]] -- is [[Help:Transclusion|transcluded]] from $1.' -- 'module-preload' --> 'Template:Documentation/preload-module-doc' -- 'create-link-display' --> 'create' -- 'create-module-doc-blurb' --> -- 'You might want to $1 a documentation page for this [[Wikipedia:Lua|Scribunto module]].' --]=] local docTitle = env.docTitle if not docTitle then return nil end local ret if docTitle.exists then -- /doc exists; link to it. local docLink = makeWikilink(docTitle.prefixedText) local editUrl = docTitle:fullUrl{action = 'edit'} local editDisplay = message('edit-link-display') local editLink = makeUrlLink(editUrl, editDisplay) local historyUrl = docTitle:fullUrl{action = 'history'} local historyDisplay = message('history-link-display') local historyLink = makeUrlLink(historyUrl, historyDisplay) ret = message('transcluded-from-blurb', {docLink}) .. ' ' .. makeToolbar(editLink, historyLink) .. '<br />' elseif env.subjectSpace == 828 then -- /doc does not exist; ask to create it. local createUrl = docTitle:fullUrl{action = 'edit', preload = message('module-preload')} local createDisplay = message('create-link-display') local createLink = makeUrlLink(createUrl, createDisplay) ret = message('create-module-doc-blurb', {createLink}) .. '<br />' end return ret end function p.makeExperimentBlurb(args, env) --[[ -- Renders the text "Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages." -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- -- Messages: -- 'sandbox-link-display' --> 'sandbox' -- 'sandbox-edit-link-display' --> 'edit' -- 'compare-link-display' --> 'diff' -- 'module-sandbox-preload' --> 'Template:Documentation/preload-module-sandbox' -- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox' -- 'sandbox-create-link-display' --> 'create' -- 'mirror-edit-summary' --> 'Create sandbox version of $1' -- 'mirror-link-display' --> 'mirror' -- 'mirror-link-preload' --> 'Template:Documentation/mirror' -- 'sandbox-link-display' --> 'sandbox' -- 'testcases-link-display' --> 'testcases' -- 'testcases-edit-link-display'--> 'edit' -- 'template-sandbox-preload' --> 'Template:Documentation/preload-sandbox' -- 'testcases-create-link-display' --> 'create' -- 'testcases-link-display' --> 'testcases' -- 'testcases-edit-link-display' --> 'edit' -- 'module-testcases-preload' --> 'Template:Documentation/preload-module-testcases' -- 'template-testcases-preload' --> 'Template:Documentation/preload-testcases' -- 'experiment-blurb-module' --> 'Editors can experiment in this module's $1 and $2 pages.' -- 'experiment-blurb-template' --> 'Editors can experiment in this template's $1 and $2 pages.' --]] local subjectSpace = env.subjectSpace local templateTitle = env.templateTitle local sandboxTitle = env.sandboxTitle local testcasesTitle = env.testcasesTitle local templatePage = templateTitle.prefixedText if not subjectSpace or not templateTitle or not sandboxTitle or not testcasesTitle then return nil end -- Make links. local sandboxLinks, testcasesLinks if sandboxTitle.exists then local sandboxPage = sandboxTitle.prefixedText local sandboxDisplay = message('sandbox-link-display') local sandboxLink = makeWikilink(sandboxPage, sandboxDisplay) local sandboxEditUrl = sandboxTitle:fullUrl{action = 'edit'} local sandboxEditDisplay = message('sandbox-edit-link-display') local sandboxEditLink = makeUrlLink(sandboxEditUrl, sandboxEditDisplay) local compareUrl = env.compareUrl local compareLink if compareUrl then local compareDisplay = message('compare-link-display') compareLink = makeUrlLink(compareUrl, compareDisplay) end sandboxLinks = sandboxLink .. ' ' .. makeToolbar(sandboxEditLink, compareLink) else local sandboxPreload if subjectSpace == 828 then sandboxPreload = message('module-sandbox-preload') else sandboxPreload = message('template-sandbox-preload') end local sandboxCreateUrl = sandboxTitle:fullUrl{action = 'edit', preload = sandboxPreload} local sandboxCreateDisplay = message('sandbox-create-link-display') local sandboxCreateLink = makeUrlLink(sandboxCreateUrl, sandboxCreateDisplay) local mirrorSummary = message('mirror-edit-summary', {makeWikilink(templatePage)}) local mirrorPreload = message('mirror-link-preload') local mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = mirrorPreload, summary = mirrorSummary} if subjectSpace == 828 then mirrorUrl = sandboxTitle:fullUrl{action = 'edit', preload = templateTitle.prefixedText, summary = mirrorSummary} end local mirrorDisplay = message('mirror-link-display') local mirrorLink = makeUrlLink(mirrorUrl, mirrorDisplay) sandboxLinks = message('sandbox-link-display') .. ' ' .. makeToolbar(sandboxCreateLink, mirrorLink) end if testcasesTitle.exists then local testcasesPage = testcasesTitle.prefixedText local testcasesDisplay = message('testcases-link-display') local testcasesLink = makeWikilink(testcasesPage, testcasesDisplay) local testcasesEditUrl = testcasesTitle:fullUrl{action = 'edit'} local testcasesEditDisplay = message('testcases-edit-link-display') local testcasesEditLink = makeUrlLink(testcasesEditUrl, testcasesEditDisplay) -- for Modules, add testcases run link if exists if testcasesTitle.contentModel == "Scribunto" and testcasesTitle.talkPageTitle and testcasesTitle.talkPageTitle.exists then local testcasesRunLinkDisplay = message('testcases-run-link-display') local testcasesRunLink = makeWikilink(testcasesTitle.talkPageTitle.prefixedText, testcasesRunLinkDisplay) testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink, testcasesRunLink) else testcasesLinks = testcasesLink .. ' ' .. makeToolbar(testcasesEditLink) end else local testcasesPreload if subjectSpace == 828 then testcasesPreload = message('module-testcases-preload') else testcasesPreload = message('template-testcases-preload') end local testcasesCreateUrl = testcasesTitle:fullUrl{action = 'edit', preload = testcasesPreload} local testcasesCreateDisplay = message('testcases-create-link-display') local testcasesCreateLink = makeUrlLink(testcasesCreateUrl, testcasesCreateDisplay) testcasesLinks = message('testcases-link-display') .. ' ' .. makeToolbar(testcasesCreateLink) end local messageName if subjectSpace == 828 then messageName = 'experiment-blurb-module' else messageName = 'experiment-blurb-template' end return message(messageName, {sandboxLinks, testcasesLinks}) end function p.makeCategoriesBlurb(args, env) --[[ -- Generates the text "Please add categories to the /doc subpage." -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- Messages: -- 'doc-link-display' --> '/doc' -- 'add-categories-blurb' --> 'Please add categories to the $1 subpage.' --]] local docTitle = env.docTitle if not docTitle then return nil end local docPathLink = makeWikilink(docTitle.prefixedText, message('doc-link-display')) return message('add-categories-blurb', {docPathLink}) end function p.makeSubpagesBlurb(args, env) --[[ -- Generates the "Subpages of this template" link. -- @args - a table of arguments passed by the user -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- Messages: -- 'template-pagetype' --> 'template' -- 'module-pagetype' --> 'module' -- 'default-pagetype' --> 'page' -- 'subpages-link-display' --> 'Subpages of this $1' --]] local subjectSpace = env.subjectSpace local templateTitle = env.templateTitle if not subjectSpace or not templateTitle then return nil end local pagetype if subjectSpace == 10 then pagetype = message('template-pagetype') elseif subjectSpace == 828 then pagetype = message('module-pagetype') else pagetype = message('default-pagetype') end local subpagesLink = makeWikilink( 'Special:PrefixIndex/' .. templateTitle.prefixedText .. '/', message('subpages-link-display', {pagetype}) ) return message('subpages-blurb', {subpagesLink}) end ---------------------------------------------------------------------------- -- Tracking categories ---------------------------------------------------------------------------- function p.addTrackingCategories(env) --[[ -- Check if {{documentation}} is transcluded on a /doc or /testcases page. -- @env - environment table containing title objects, etc., generated with p.getEnvironment -- Messages: -- 'display-strange-usage-category' --> true -- 'doc-subpage' --> 'doc' -- 'testcases-subpage' --> 'testcases' -- 'strange-usage-category' --> 'Wikipedia pages with strange ((documentation)) usage' -- -- /testcases pages in the module namespace are not categorised, as they may have -- {{documentation}} transcluded automatically. --]] local title = env.title local subjectSpace = env.subjectSpace if not title or not subjectSpace then return nil end local subpage = title.subpageText local ret = '' if message('display-strange-usage-category', nil, 'boolean') and ( subpage == message('doc-subpage') or subjectSpace ~= 828 and subpage == message('testcases-subpage') ) then ret = ret .. makeCategoryLink(message('strange-usage-category')) end return ret end return p 78cc3a78f2b5dbb267fa16027c0800a22dbd3c42 Module:Arguments 828 21 36 35 2023-08-27T19:29:38Z Alxira5 4 1 revision imported: Module imported from Miraheze Developers, published under the CC-BY-SA 4.0 license Scribunto text/plain -- This module provides easy processing of arguments passed to Scribunto from -- #invoke. It is intended for use by other Lua modules, and should not be -- called from #invoke directly. local libraryUtil = require('libraryUtil') local checkType = libraryUtil.checkType local arguments = {} -- Generate four different tidyVal functions, so that we don't have to check the -- options every time we call it. local function tidyValDefault(key, val) if type(val) == 'string' then val = val:match('^%s*(.-)%s*$') if val == '' then return nil else return val end else return val end end local function tidyValTrimOnly(key, val) if type(val) == 'string' then return val:match('^%s*(.-)%s*$') else return val end end local function tidyValRemoveBlanksOnly(key, val) if type(val) == 'string' then if val:find('%S') then return val else return nil end else return val end end local function tidyValNoChange(key, val) return val end local function matchesTitle(given, title) local tp = type( given ) return (tp == 'string' or tp == 'number') and mw.title.new( given ).prefixedText == title end local translate_mt = { __index = function(t, k) return k end } function arguments.getArgs(frame, options) checkType('getArgs', 1, frame, 'table', true) checkType('getArgs', 2, options, 'table', true) frame = frame or {} options = options or {} --[[ -- Set up argument translation. --]] options.translate = options.translate or {} if getmetatable(options.translate) == nil then setmetatable(options.translate, translate_mt) end if options.backtranslate == nil then options.backtranslate = {} for k,v in pairs(options.translate) do options.backtranslate[v] = k end end if options.backtranslate and getmetatable(options.backtranslate) == nil then setmetatable(options.backtranslate, { __index = function(t, k) if options.translate[k] ~= k then return nil else return k end end }) end --[[ -- Get the argument tables. If we were passed a valid frame object, get the -- frame arguments (fargs) and the parent frame arguments (pargs), depending -- on the options set and on the parent frame's availability. If we weren't -- passed a valid frame object, we are being called from another Lua module -- or from the debug console, so assume that we were passed a table of args -- directly, and assign it to a new variable (luaArgs). --]] local fargs, pargs, luaArgs if type(frame.args) == 'table' and type(frame.getParent) == 'function' then if options.wrappers then --[[ -- The wrappers option makes Module:Arguments look up arguments in -- either the frame argument table or the parent argument table, but -- not both. This means that users can use either the #invoke syntax -- or a wrapper template without the loss of performance associated -- with looking arguments up in both the frame and the parent frame. -- Module:Arguments will look up arguments in the parent frame -- if it finds the parent frame's title in options.wrapper; -- otherwise it will look up arguments in the frame object passed -- to getArgs. --]] local parent = frame:getParent() if not parent then fargs = frame.args else local title = parent:getTitle():gsub('/sandbox$', '') local found = false if matchesTitle(options.wrappers, title) then found = true elseif type(options.wrappers) == 'table' then for _,v in pairs(options.wrappers) do if matchesTitle(v, title) then found = true break end end end -- We test for false specifically here so that nil (the default) acts like true. if found or options.frameOnly == false then pargs = parent.args end if not found or options.parentOnly == false then fargs = frame.args end end else -- options.wrapper isn't set, so check the other options. if not options.parentOnly then fargs = frame.args end if not options.frameOnly then local parent = frame:getParent() pargs = parent and parent.args or nil end end if options.parentFirst then fargs, pargs = pargs, fargs end else luaArgs = frame end -- Set the order of precedence of the argument tables. If the variables are -- nil, nothing will be added to the table, which is how we avoid clashes -- between the frame/parent args and the Lua args. local argTables = {fargs} argTables[#argTables + 1] = pargs argTables[#argTables + 1] = luaArgs --[[ -- Generate the tidyVal function. If it has been specified by the user, we -- use that; if not, we choose one of four functions depending on the -- options chosen. This is so that we don't have to call the options table -- every time the function is called. --]] local tidyVal = options.valueFunc if tidyVal then if type(tidyVal) ~= 'function' then error( "bad value assigned to option 'valueFunc'" .. '(function expected, got ' .. type(tidyVal) .. ')', 2 ) end elseif options.trim ~= false then if options.removeBlanks ~= false then tidyVal = tidyValDefault else tidyVal = tidyValTrimOnly end else if options.removeBlanks ~= false then tidyVal = tidyValRemoveBlanksOnly else tidyVal = tidyValNoChange end end --[[ -- Set up the args, metaArgs and nilArgs tables. args will be the one -- accessed from functions, and metaArgs will hold the actual arguments. Nil -- arguments are memoized in nilArgs, and the metatable connects all of them -- together. --]] local args, metaArgs, nilArgs, metatable = {}, {}, {}, {} setmetatable(args, metatable) local function mergeArgs(tables) --[[ -- Accepts multiple tables as input and merges their keys and values -- into one table. If a value is already present it is not overwritten; -- tables listed earlier have precedence. We are also memoizing nil -- values, which can be overwritten if they are 's' (soft). --]] for _, t in ipairs(tables) do for key, val in pairs(t) do if metaArgs[key] == nil and nilArgs[key] ~= 'h' then local tidiedVal = tidyVal(key, val) if tidiedVal == nil then nilArgs[key] = 's' else metaArgs[key] = tidiedVal end end end end end --[[ -- Define metatable behaviour. Arguments are memoized in the metaArgs table, -- and are only fetched from the argument tables once. Fetching arguments -- from the argument tables is the most resource-intensive step in this -- module, so we try and avoid it where possible. For this reason, nil -- arguments are also memoized, in the nilArgs table. Also, we keep a record -- in the metatable of when pairs and ipairs have been called, so we do not -- run pairs and ipairs on the argument tables more than once. We also do -- not run ipairs on fargs and pargs if pairs has already been run, as all -- the arguments will already have been copied over. --]] metatable.__index = function (t, key) --[[ -- Fetches an argument when the args table is indexed. First we check -- to see if the value is memoized, and if not we try and fetch it from -- the argument tables. When we check memoization, we need to check -- metaArgs before nilArgs, as both can be non-nil at the same time. -- If the argument is not present in metaArgs, we also check whether -- pairs has been run yet. If pairs has already been run, we return nil. -- This is because all the arguments will have already been copied into -- metaArgs by the mergeArgs function, meaning that any other arguments -- must be nil. --]] if type(key) == 'string' then key = options.translate[key] end local val = metaArgs[key] if val ~= nil then return val elseif metatable.donePairs or nilArgs[key] then return nil end for _, argTable in ipairs(argTables) do local argTableVal = tidyVal(key, argTable[key]) if argTableVal ~= nil then metaArgs[key] = argTableVal return argTableVal end end nilArgs[key] = 'h' return nil end metatable.__newindex = function (t, key, val) -- This function is called when a module tries to add a new value to the -- args table, or tries to change an existing value. if type(key) == 'string' then key = options.translate[key] end if options.readOnly then error( 'could not write to argument table key "' .. tostring(key) .. '"; the table is read-only', 2 ) elseif options.noOverwrite and args[key] ~= nil then error( 'could not write to argument table key "' .. tostring(key) .. '"; overwriting existing arguments is not permitted', 2 ) elseif val == nil then --[[ -- If the argument is to be overwritten with nil, we need to erase -- the value in metaArgs, so that __index, __pairs and __ipairs do -- not use a previous existing value, if present; and we also need -- to memoize the nil in nilArgs, so that the value isn't looked -- up in the argument tables if it is accessed again. --]] metaArgs[key] = nil nilArgs[key] = 'h' else metaArgs[key] = val end end local function translatenext(invariant) local k, v = next(invariant.t, invariant.k) invariant.k = k if k == nil then return nil elseif type(k) ~= 'string' or not options.backtranslate then return k, v else local backtranslate = options.backtranslate[k] if backtranslate == nil then -- Skip this one. This is a tail call, so this won't cause stack overflow return translatenext(invariant) else return backtranslate, v end end end metatable.__pairs = function () -- Called when pairs is run on the args table. if not metatable.donePairs then mergeArgs(argTables) metatable.donePairs = true end return translatenext, { t = metaArgs } end local function inext(t, i) -- This uses our __index metamethod local v = t[i + 1] if v ~= nil then return i + 1, v end end metatable.__ipairs = function (t) -- Called when ipairs is run on the args table. return inext, t, 0 end return args end return arguments 3134ecce8429b810d445e29eae115e2ae4c36c53 Module:Documentation/config 828 22 38 37 2023-08-27T20:01:04Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license Scribunto text/plain ---------------------------------------------------------------------------------------------------- -- -- Configuration for Module:Documentation -- -- Here you can set the values of the parameters and messages used in Module:Documentation to -- localise it to your wiki and your language. Unless specified otherwise, values given here -- should be string values. ---------------------------------------------------------------------------------------------------- local cfg = {} -- Do not edit this line. ---------------------------------------------------------------------------------------------------- -- Start box configuration ---------------------------------------------------------------------------------------------------- -- cfg['documentation-icon-wikitext'] -- The wikitext for the icon shown at the top of the template. cfg['documentation-icon-wikitext'] = '[[File:Test Template Info-Icon - Version (2).svg|50px|link=|alt=]]' -- cfg['template-namespace-heading'] -- The heading shown in the template namespace. cfg['template-namespace-heading'] = 'Template documentation' -- cfg['module-namespace-heading'] -- The heading shown in the module namespace. cfg['module-namespace-heading'] = 'Module documentation' -- cfg['file-namespace-heading'] -- The heading shown in the file namespace. cfg['file-namespace-heading'] = 'Summary' -- cfg['other-namespaces-heading'] -- The heading shown in other namespaces. cfg['other-namespaces-heading'] = 'Documentation' -- cfg['view-link-display'] -- The text to display for "view" links. cfg['view-link-display'] = 'view' -- cfg['edit-link-display'] -- The text to display for "edit" links. cfg['edit-link-display'] = 'edit' -- cfg['history-link-display'] -- The text to display for "history" links. cfg['history-link-display'] = 'history' -- cfg['purge-link-display'] -- The text to display for "purge" links. cfg['purge-link-display'] = 'purge' -- cfg['create-link-display'] -- The text to display for "create" links. cfg['create-link-display'] = 'create' ---------------------------------------------------------------------------------------------------- -- Link box (end box) configuration ---------------------------------------------------------------------------------------------------- -- cfg['transcluded-from-blurb'] -- Notice displayed when the docs are transcluded from another page. $1 is a wikilink to that page. cfg['transcluded-from-blurb'] = 'The above [[w:Wikipedia:Template documentation|documentation]] is [[mw:Help:Transclusion|transcluded]] from $1.' --[[ -- cfg['create-module-doc-blurb'] -- Notice displayed in the module namespace when the documentation subpage does not exist. -- $1 is a link to create the documentation page with the preload cfg['module-preload'] and the -- display cfg['create-link-display']. --]] cfg['create-module-doc-blurb'] = 'You might want to $1 a documentation page for this [[mw:Extension:Scribunto/Lua reference manual|Scribunto module]].' ---------------------------------------------------------------------------------------------------- -- Experiment blurb configuration ---------------------------------------------------------------------------------------------------- --[[ -- cfg['experiment-blurb-template'] -- cfg['experiment-blurb-module'] -- The experiment blurb is the text inviting editors to experiment in sandbox and test cases pages. -- It is only shown in the template and module namespaces. With the default English settings, it -- might look like this: -- -- Editors can experiment in this template's sandbox (edit | diff) and testcases (edit) pages. -- -- In this example, "sandbox", "edit", "diff", "testcases", and "edit" would all be links. -- -- There are two versions, cfg['experiment-blurb-template'] and cfg['experiment-blurb-module'], depending -- on what namespace we are in. -- -- Parameters: -- -- $1 is a link to the sandbox page. If the sandbox exists, it is in the following format: -- -- cfg['sandbox-link-display'] (cfg['sandbox-edit-link-display'] | cfg['compare-link-display']) -- -- If the sandbox doesn't exist, it is in the format: -- -- cfg['sandbox-link-display'] (cfg['sandbox-create-link-display'] | cfg['mirror-link-display']) -- -- The link for cfg['sandbox-create-link-display'] link preloads the page with cfg['template-sandbox-preload'] -- or cfg['module-sandbox-preload'], depending on the current namespace. The link for cfg['mirror-link-display'] -- loads a default edit summary of cfg['mirror-edit-summary']. -- -- $2 is a link to the test cases page. If the test cases page exists, it is in the following format: -- -- cfg['testcases-link-display'] (cfg['testcases-edit-link-display'] | cfg['testcases-run-link-display']) -- -- If the test cases page doesn't exist, it is in the format: -- -- cfg['testcases-link-display'] (cfg['testcases-create-link-display']) -- -- If the test cases page doesn't exist, the link for cfg['testcases-create-link-display'] preloads the -- page with cfg['template-testcases-preload'] or cfg['module-testcases-preload'], depending on the current -- namespace. --]] cfg['experiment-blurb-template'] = "Editors can experiment in this template's $1 and $2 pages." cfg['experiment-blurb-module'] = "Editors can experiment in this module's $1 and $2 pages." ---------------------------------------------------------------------------------------------------- -- Sandbox link configuration ---------------------------------------------------------------------------------------------------- -- cfg['sandbox-subpage'] -- The name of the template subpage typically used for sandboxes. cfg['sandbox-subpage'] = 'sandbox' -- cfg['template-sandbox-preload'] -- Preload file for template sandbox pages. cfg['template-sandbox-preload'] = 'Template:Documentation/preload-sandbox' -- cfg['module-sandbox-preload'] -- Preload file for Lua module sandbox pages. cfg['module-sandbox-preload'] = 'Template:Documentation/preload-module-sandbox' -- cfg['sandbox-link-display'] -- The text to display for "sandbox" links. cfg['sandbox-link-display'] = 'sandbox' -- cfg['sandbox-edit-link-display'] -- The text to display for sandbox "edit" links. cfg['sandbox-edit-link-display'] = 'edit' -- cfg['sandbox-create-link-display'] -- The text to display for sandbox "create" links. cfg['sandbox-create-link-display'] = 'create' -- cfg['compare-link-display'] -- The text to display for "compare" links. cfg['compare-link-display'] = 'diff' -- cfg['mirror-edit-summary'] -- The default edit summary to use when a user clicks the "mirror" link. $1 is a wikilink to the -- template page. cfg['mirror-edit-summary'] = 'Create sandbox version of $1' -- cfg['mirror-link-display'] -- The text to display for "mirror" links. cfg['mirror-link-display'] = 'mirror' -- cfg['mirror-link-preload'] -- The page to preload when a user clicks the "mirror" link. cfg['mirror-link-preload'] = 'Template:Documentation/mirror' ---------------------------------------------------------------------------------------------------- -- Test cases link configuration ---------------------------------------------------------------------------------------------------- -- cfg['testcases-subpage'] -- The name of the template subpage typically used for test cases. cfg['testcases-subpage'] = 'testcases' -- cfg['template-testcases-preload'] -- Preload file for template test cases pages. cfg['template-testcases-preload'] = 'Template:Documentation/preload-testcases' -- cfg['module-testcases-preload'] -- Preload file for Lua module test cases pages. cfg['module-testcases-preload'] = 'Template:Documentation/preload-module-testcases' -- cfg['testcases-link-display'] -- The text to display for "testcases" links. cfg['testcases-link-display'] = 'testcases' -- cfg['testcases-edit-link-display'] -- The text to display for test cases "edit" links. cfg['testcases-edit-link-display'] = 'edit' -- cfg['testcases-run-link-display'] -- The text to display for test cases "run" links. cfg['testcases-run-link-display'] = 'run' -- cfg['testcases-create-link-display'] -- The text to display for test cases "create" links. cfg['testcases-create-link-display'] = 'create' ---------------------------------------------------------------------------------------------------- -- Add categories blurb configuration ---------------------------------------------------------------------------------------------------- --[[ -- cfg['add-categories-blurb'] -- Text to direct users to add categories to the /doc subpage. Not used if the "content" or -- "docname fed" arguments are set, as then it is not clear where to add the categories. $1 is a -- link to the /doc subpage with a display value of cfg['doc-link-display']. --]] cfg['add-categories-blurb'] = 'Add categories to the $1 subpage.' -- cfg['doc-link-display'] -- The text to display when linking to the /doc subpage. cfg['doc-link-display'] = '/doc' ---------------------------------------------------------------------------------------------------- -- Subpages link configuration ---------------------------------------------------------------------------------------------------- --[[ -- cfg['subpages-blurb'] -- The "Subpages of this template" blurb. $1 is a link to the main template's subpages with a -- display value of cfg['subpages-link-display']. In the English version this blurb is simply -- the link followed by a period, and the link display provides the actual text. --]] cfg['subpages-blurb'] = '$1.' --[[ -- cfg['subpages-link-display'] -- The text to display for the "subpages of this page" link. $1 is cfg['template-pagetype'], -- cfg['module-pagetype'] or cfg['default-pagetype'], depending on whether the current page is in -- the template namespace, the module namespace, or another namespace. --]] cfg['subpages-link-display'] = 'Subpages of this $1' -- cfg['template-pagetype'] -- The pagetype to display for template pages. cfg['template-pagetype'] = 'template' -- cfg['module-pagetype'] -- The pagetype to display for Lua module pages. cfg['module-pagetype'] = 'module' -- cfg['default-pagetype'] -- The pagetype to display for pages other than templates or Lua modules. cfg['default-pagetype'] = 'page' ---------------------------------------------------------------------------------------------------- -- Doc link configuration ---------------------------------------------------------------------------------------------------- -- cfg['doc-subpage'] -- The name of the subpage typically used for documentation pages. cfg['doc-subpage'] = 'doc' -- cfg['docpage-preload'] -- Preload file for template documentation pages in all namespaces. cfg['docpage-preload'] = 'Template:Documentation/preload' -- cfg['module-preload'] -- Preload file for Lua module documentation pages. cfg['module-preload'] = 'Template:Documentation/preload-module-doc' ---------------------------------------------------------------------------------------------------- -- HTML and CSS configuration ---------------------------------------------------------------------------------------------------- -- cfg['templatestyles'] -- The name of the TemplateStyles page where CSS is kept. -- Sandbox CSS will be at Module:Documentation/sandbox/styles.css when needed. cfg['templatestyles'] = 'Module:Documentation/styles.css' -- cfg['container'] -- Class which can be used to set flex or grid CSS on the -- two child divs documentation and documentation-metadata cfg['container'] = 'documentation-container' -- cfg['main-div-classes'] -- Classes added to the main HTML "div" tag. cfg['main-div-classes'] = 'documentation' -- cfg['main-div-heading-class'] -- Class for the main heading for templates and modules and assoc. talk spaces cfg['main-div-heading-class'] = 'documentation-heading' -- cfg['start-box-class'] -- Class for the start box cfg['start-box-class'] = 'documentation-startbox' -- cfg['start-box-link-classes'] -- Classes used for the [view][edit][history] or [create] links in the start box. -- mw-editsection-like is per [[Wikipedia:Village pump (technical)/Archive 117]] cfg['start-box-link-classes'] = 'mw-editsection-like plainlinks' -- cfg['end-box-class'] -- Class for the end box. cfg['end-box-class'] = 'documentation-metadata' -- cfg['end-box-plainlinks'] -- Plainlinks cfg['end-box-plainlinks'] = 'plainlinks' -- cfg['toolbar-class'] -- Class added for toolbar links. cfg['toolbar-class'] = 'documentation-toolbar' -- cfg['clear'] -- Just used to clear things. cfg['clear'] = 'documentation-clear' ---------------------------------------------------------------------------------------------------- -- Tracking category configuration ---------------------------------------------------------------------------------------------------- -- cfg['display-strange-usage-category'] -- Set to true to enable output of cfg['strange-usage-category'] if the module is used on a /doc subpage -- or a /testcases subpage. This should be a boolean value (either true or false). cfg['display-strange-usage-category'] = true -- cfg['strange-usage-category'] -- Category to output if cfg['display-strange-usage-category'] is set to true and the module is used on a -- /doc subpage or a /testcases subpage. cfg['strange-usage-category'] = 'Wikipedia pages with strange ((documentation)) usage' --[[ ---------------------------------------------------------------------------------------------------- -- End configuration -- -- Don't edit anything below this line. ---------------------------------------------------------------------------------------------------- --]] return cfg d70e8b1402a2bbe08a1fef4b75d743e661af0c95 Template:Header 10 23 40 39 2023-08-27T20:01:05Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki {| style="width: 100% !important;" |- | style="border-top: 4px solid #{{{topbarhex|6F6F6F}}}; background-color: #{{{bodyhex|F6F6F6}}}; padding: 10px 15px;" | {{#if:{{{shortcut|}}}| {{shortcut|{{{shortcut|uselang={{{uselang|{{CURRENTCONTENTLANGUAGE}}}}}}}}}}}}<div style="font-size:180%; text-align: left; color: {{{titlecolor|}}}">'''{{{title|{{{1|{{BASEPAGENAME}}}}}}}}'''</div> <div style="padding-top:0.3em; padding-bottom:0.1em; font-size:100%; text-align: left; color: {{{bodycolor|}}}">{{{notes|Put some notes here!}}}</div> |- | style="height: 10px" | |} {{clear}}<noinclude>{{documentation}}[[Category:templates]]</noinclude> 03aac86137ab11bfccbcceb2de919475af2953dd Template:Agree 10 24 42 41 2023-08-27T20:01:06Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Symbol confirmed.svg|18px|link=]] '''{{{1|Agree}}}'''<noinclude>{{documentation}}</noinclude> 775ddedaccda0d477a1b3c82d422e3760c862609 Template:Comment 10 25 44 43 2023-08-27T20:01:06Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Pictogram voting comment.svg|18px|link=]]&nbsp;'''{{{1|Comment:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude> 5f48a21f6ec3dc6d82cfa7a668c9e00fec175396 Template:Doing 10 26 46 45 2023-08-27T20:01:07Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Pictogram voting wait.svg|18px|link=|alt=]]&nbsp;'''{{{1|Doing…}}}'''<noinclude> {{documentation}} [[Category:Resolution templates]] </noinclude> eb1feacb3d7a14829c6a9be311732ee0d24d35f3 Template:Custom resolution 10 27 48 47 2023-08-27T20:01:08Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki <span class="nowrap">[[File:{{{1|Cancelled process mini.svg}}}|18px|alt={{{2|Text here}}}]] <span style="{{{3|">'''{{{2|Text here}}}'''</span></span> <noinclude>{{Documentation|content= This template allows for the creation of custom [[Template:Template list#Resolution templates|resolution templates]] using 2 parameters. }}[[Category:Resolution templates]]</noinclude> a563b1f700886c4f97480a7ee81988b33af01ccf Template:Not done 10 28 50 49 2023-08-27T20:01:09Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:X mark.svg|18px]] '''{{{1|Not done}}}'''<noinclude> {{documentation}} [[Category:Resolution templates]] </noinclude> fd025e245bee74ddd6c5ae757f983a3cd3258d94 Template:Done 10 29 52 51 2023-08-27T20:01:09Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki <span class="nowrap">[[File:Yes check.svg|18px|link=|alt=]]&nbsp;'''{{{1|Done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|&#58; {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude> {{documentation}} [[Category:Resolution templates]]</noinclude> 717c1385d516cd84dc05a10ba88359a52c9d8415 Template:Note 10 30 54 53 2023-08-27T20:01:10Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Pictogram voting info.svg|18px|link=]]&nbsp;'''{{{1|Note:}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 4d5cae62908f9cc8da2988712b236fe939bc80e2 Template:Endorse 10 31 56 55 2023-08-27T20:01:11Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Symbol support2 vote.svg|link=|alt=|16px|]]&nbsp;'''{{{1|Endorse}}}'''<noinclude>{{Documentation}}</noinclude> 23cc6c948818ca6949bd9af0991af58f1858483f Template:High priority 10 32 58 57 2023-08-27T20:01:11Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Exclamationdiamond.svg|20px|link=]]&nbsp;'''{{{1|High Priority}}}'''{{{{{|safesubst:}}}#if:{{{note|{{{reason|}}}}}}|<nowiki />: {{{note|{{{reason}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 65d49ca7f928fef46651d89d894267497560a60b Template:Idea 10 33 60 59 2023-08-27T20:01:12Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Dialog-information on.svg|18px|link=]] '''{{{1|Idea}}}:'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> e4062daed60634ce9e9cd2f052d9102bcf7e2916 Template:In progress 10 34 62 61 2023-08-27T20:01:13Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki [[File:Pictogram voting info.svg|16px|link=|alt=]]&nbsp;'''{{{1|In Progress}}}'''<noinclude>{{documentation}}</noinclude> ff9e3ff4245b3dcc9ae45a1f8e3c7e7830fc0fff Special:Badtitle/NS200:Alxira5 200 35 63 2023-08-27T20:03:41Z Alxira5 4 import user wiki wikitext text/x-wiki da39a3ee5e6b4b0d3255bfef95601890afd80709 Template:Clear 10 36 65 64 2023-08-28T02:15:01Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <div style="clear:{{{1|both}}};"></div><noinclude> {{documentation}} </noinclude> 38bab3e3d7fbd3d6800d46556e60bc6bac494d72 Template:On hold 10 37 67 66 2023-08-28T02:15:02Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki [[File:Symbol wait.svg|18px]] '''{{{1|On hold}}}'''<noinclude>[[Category:Resolution templates]]<noinclude> {{documentation}} [[Category:Resolution templates]]</noinclude> 7a18e8aa8c80a33b1a68eed60d0993a75202162f Template:Partly done 10 38 69 68 2023-08-28T02:15:02Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <span class="nowrap">[[File:Yellow_check.svg|18px|link=|alt=]]&nbsp;'''{{{1|Partly done}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|&#58; {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 24a90b5a5c4c716b7ec12889fbd09a1da2ba1ca3 Template:Pending 10 39 71 70 2023-08-28T02:15:03Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{On hold|{{{1|Pending}}}}}<noinclude>{{Documentation}}</noinclude> 3d534f8f2cf14f73be843d306efcecbff05c7f5e Template:Question 10 40 73 72 2023-08-28T02:15:03Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki [[File:Pictogram voting question.svg|18px|link=]]&nbsp;'''{{{1|Question:}}}'''<noinclude>{{documentation}} [[Category:Resolution templates]]</noinclude> 9fae3d5ccc70d95a5a7de8983d7a82c1a55853e3 Template:Resolved 10 41 75 74 2023-08-28T02:15:04Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <span class="nowrap">[[File:Yes check.svg|18px|link=]]&nbsp;'''{{{1|Resolved}}}'''</span>{{{{{|safesubst:}}}#if:{{{2|{{{note|{{{reason|}}}}}}}}}|&#58; {{{2|{{{note|{{{reason}}}}}}}}}}}<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> bcebb832c81fc395e8891f82747510f76292cb34 Template:Reviewing 10 42 77 76 2023-08-28T02:15:04Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki [[File:Pictogram voting wait green.svg|17px|link=]] '''{{{1|Reviewing}}}...'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 0184f75a66f991d9eb99f23a75df36dd184e0c4b Template:Thank you 10 43 79 78 2023-08-28T02:15:05Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <span class="nowrap">[[File:Face-smile.svg|18px|link=]] '''{{{1|Thank you}}}'''</span><noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 4312420b6485d1eb316af5c56f663a7d618afb9b Template:Withdrawn 10 44 81 80 2023-08-28T02:15:05Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki [[File:Cancelled process mini.svg|200x20px|link=|alt=]]&nbsp;'''{{{1|Request withdrawn}}}'''<noinclude>{{documentation}}</noinclude> 24c0cd218d3a61ac8b524c6f8d1b5cc405ca3d80 Template:Working 10 45 83 82 2023-08-28T02:15:06Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki [[File:Icon tools.svg|20px|link=]]&nbsp;'''{{{1|Working}}}'''<noinclude>{{documentation}}[[Category:Resolution templates]]</noinclude> 0619210f08d5114b9a348b4f1045a0b6f4552012 Template:Documentation/mirror 10 46 85 84 2023-08-28T02:15:06Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <includeonly>{{subst:msgnw:{{subst:NAMESPACE}}:{{subst:BASEPAGENAME}}}}</includeonly><noinclude>{{doc}}</noinclude> 2f3df1c981931719e821f054f3db0c67072f781e Template:Documentation/preload 10 47 87 86 2023-08-28T02:15:06Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{Documentation subpage}} == Usage == <include<includeonly></includeonly>only> <!-- Categories below this line --> }}</include<includeonly></includeonly>only><noinclude> {{Documentation|content= This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template documentation page. [[Category:Documentation preloads]] }}</noinclude> 587413cafd960a3f7f3c9257a4160f3654fdbe0f Template:Documentation/preload-sandbox 10 48 89 88 2023-08-28T02:15:07Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <!-- Add your experimental template code here. --><noin<includeonly></includeonly>clude> {{Documentation}} </noin<includeonly></includeonly>clude> c7b7f3f85c510513f7415b512c03742fe61ee31a Template:Documentation/preload-testcases 10 49 91 90 2023-08-28T02:15:08Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki This page contains test cases for {{tlx|{{<includeonly>safesubst:</includeonly>BASEPAGENAME}}}}.<noinclude>{{documentation|content=This page contains the default wikitext that appears when an editor clicks "create" to begin creating a new template testcases page.}}[[Category:Documentation preloads]]</noinclude> 9e9c7d43373bd7aeaee3293842ae54db0257e549 Template:Delete 10 50 93 92 2023-08-28T02:15:08Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{MessageBox |Flag color=firebrick |Border color=firebrick |Background color=#FFEEEE |Image=[[File:Trash Can.svg|80px]] |Message text=<span style="line-height:2;"><span style="color:red; line-height:1.2;">'''This article is a candidate for speedy deletion because {{{1}}}. '''</span><br><span style="color:#000000;"> Deleting Reason: {{{1|No reason given}}}</span></span> }}<noinclude>[[Category:Notice templates]]</noinclude> <includeonly>[[Category:Candidates for deletion]]</includeonly> <noinclude> <languages /> <translate> <!--T:1--> == Usage Note == Add this template to any page on this wiki for which you're requesting an [[<tvar name=admin>mw:Special:MyLanguage/Manual:Administrators</tvar>|administrator]] to delete, either by adding it to the very top of the page (preferred) or by replacing the existing content with this template (also acceptable) following the format prescribed below.</translate> <code><nowiki>{{Delete|1=</nowiki>''Your deletion reason''<nowiki>}}</nowiki></code> <translate> <!--T:2--> Replace ''your deletion reason'' with one of the commonly accepted reasons for deletion below, or describe concisely ''why'' you are requesting deletion. <!--T:3--> If you do not specify a reason in parameter <code>1=</code>, ''no deletion reason'' will be inserted, and your request ''may'' be declined if it is not apparent why deletion is being requested. <!--T:4--> === Commonly accepted reasons for deletion === * Vandalism * Attack page/page created solely for harassment * Copyright violation * Spam * Test page. Please either use the [[<tvar name=sb>m:Meta:Sandbox</tvar>|community sandbox]] or [[<tvar name=mypsb>Special:MyPage/sandbox</tvar>|create your personal sandbox]] * Non-controversial housekeeping * [[<tvar name=br>Special:BrokenRedirects</tvar>|Broken redirect]] * [[<tvar name=dr>Special:DoubleRedirects</tvar>|Double redirect]] * Author requests deletion, or author blanked * Subpages with no parent page * Talk pages with no companion page and no meaningful discussion history * Images available as identical copies on either [[<tvar name=commons>commons:Special:MyLanguage/Main Page</tvar>|Miraheze Commons]] or [[wikimediacommons:|Wikimedia Commons]] * [[<tvar name=uc>Special:UnusedCategories</tvar>|Empty category]] * [[<tvar name=ut>Special:UnusedTemplates</tvar>|Unused template (including a template redirect)]] with no inlinks, transclusions, or page watchers === Parameter(s) === <!--T:5--> </translate> <templatedata> { "params": { "1": { "label": "Deletion reason", "example": "Author requests deletion, or author blanked", "default": "No deletion reason", "suggested": true, "description": "Template to add to any page requiring deletion." } } } </templatedata> </noinclude> ae465d4609d3bbb646339e90ae469c31ce34a9df Template:Userbox 10 51 95 94 2023-08-28T02:15:09Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{#invoke:userbox|userbox}}<noinclude>{{documentation}}</noinclude> 6813e8e31cadc62df2379b5fae9ea23c23f29e97 Template:Information 10 52 97 96 2023-08-28T02:15:09Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <templatestyles src="Information/style.css" /> <div class="hproduct commons-file-information-table"> <table class="toccolours vevent fileinfotpl-type-information" style="width: 100%;" cellpadding="4"> <!-- Description --> <tr style="vertical-align: top"> <td id="fileinfotpl_desc" class="fileinfo-paramfield">Description<span class="summary fn" style="display:none">{{PAGENAME}}</span></td> <td class="description">{{ #if: {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{{description|{{{Description|{{{Descripción|{{{descripción|}}}}}}}}}}}} | {{Description missing}} }}</td> </tr> <!-- Source --> <tr style="vertical-align: top"> <td id="fileinfotpl_src" class="fileinfo-paramfield">Source</td> <td>{{ #if: {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{{source|{{{Source|{{{fuente|{{{Fuente|}}}}}}}}}}}} | {{Description missing|source information}} }}</td> </tr> <!-- Author --> <tr style="vertical-align: top"> <td id="fileinfotpl_aut" class="fileinfo-paramfield">Author</td> <td>{{ #if: {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{{author|{{{Author|{{{autor|{{{Autor|}}}}}}}}}}}} | {{Description missing|author information}} }}</td> </tr> <!-- Fecha --> <tr style="vertical-align: top"> <td id="fileinfotpl_aut" class="fileinfo-paramfield">Date</td> <td>{{{date|{{{Date|{{{fecha|{{{Fecha|}}}}}}}}}}}}</td> </tr> <!-- Other versions --> {{#switch: {{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}}{{{demo|<noinclude>1</noinclude>}}} | = | - = | none = | #default = <tr style="vertical-align: top"> <td id="fileinfotpl_ver" class="fileinfo-paramfield" style="background: #ccf; text-align: right; padding-right: 0.4em; width: 15%; font-weight:bold">Other versions</td> <td> {{{other_versions|{{{Other_versions|{{{other versions|{{{Other versions|}}} }}} }}} }}} </td> </tr> }} </table> </div><noinclude>{{Documentation}}</noinclude> 3a749131ebeff40d99673728a045f76f81456cc1 Template:Information/style.css 10 53 99 98 2023-08-28T02:15:10Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 text text/plain .fileinfo-paramfield { background: #ccf; text-align: right; padding-right: 0.4em; width: 15%; font-weight: bold; } /* [[Category:Template stylesheets]] */ 396fcf8276bedcc9dad608bdbd9bf1be7f90424d Template:Talk quote inline 10 54 101 100 2023-08-28T02:15:10Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <templatestyles src="Talk quote inline/styles.css" /><!-- --><q {{#if: {{{title|}}} | title="{{{title}}}"}} class="inline-quote-talk {{#if: {{{i|{{{italic|}}}}}} | inline-quote-talk-italic}} {{#if: {{{q|{{{quotes|}}}}}}|inline-quote-talk-marks}}">{{{1|Example text}}}</q><!-- --><noinclude> {{Documentation}} </noinclude> b18e2fdc57277adbf4e3f4f513e78ecc5831453f Template:Soft redirect 10 55 103 102 2023-08-28T02:15:11Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki __NONEWSECTIONLINK__[[File:Softredirarrow.svg|64px|Soft redirect to:|link=]]<span class="redirectText" id="softredirect">[[:{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}|{{{2|{{#invoke:String|match|1={{{1}}}|2=^:*(.-)$}}}}}]]</span><br /><span style="font-size:85%; padding-left:48px;">This page is a [[metawikimedia:soft redirect|soft redirect]].</span><noinclude> {{Documentation}} </noinclude> a965c0fe43aa0fe8f0e17ed40d725f0e7b3649f6 Template:See also 10 56 105 104 2023-08-28T02:15:11Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{hatnote|extraclasses=boilerplate seealso|{{{altphrase|See also}}}: {{#if:{{{1<includeonly>|</includeonly>}}} |<!--then:-->[[:{{{1}}}{{#if:{{{label 1|{{{l1|}}}}}}|{{!}}{{{label 1|{{{l1}}}}}}}}]] |<!--else:-->'''Error: [[Template:See also|Template must be given at least one article name]]''' }}{{#if:{{{2|}}}|{{#if:{{{3|}}}|, |&nbsp;and }} [[:{{{2}}}{{#if:{{{label 2|{{{l2|}}}}}}|{{!}}{{{label 2|{{{l2}}}}}}}}]] }}{{#if:{{{3|}}}|{{#if:{{{4|}}}|, |,&nbsp;and }} [[:{{{3}}}{{#if:{{{label 3|{{{l3|}}}}}}|{{!}}{{{label 3|{{{l3}}}}}}}}]] }}{{#if:{{{4|}}}|{{#if:{{{5|}}}|, |,&nbsp;and }} [[:{{{4}}}{{#if:{{{label 4|{{{l4|}}}}}}|{{!}}{{{label 4|{{{l4}}}}}}}}]] }}{{#if:{{{5|}}}|{{#if:{{{6|}}}|, |,&nbsp;and }} [[:{{{5}}}{{#if:{{{label 5|{{{l5|}}}}}}|{{!}}{{{label 5|{{{l5}}}}}}}}]] }}{{#if:{{{6|}}}|{{#if:{{{7|}}}|, |,&nbsp;and }} [[:{{{6}}}{{#if:{{{label 6|{{{l6|}}}}}}|{{!}}{{{label 6|{{{l6}}}}}}}}]] }}{{#if:{{{7|}}}|{{#if:{{{8|}}}|, |,&nbsp;and }} [[:{{{7}}}{{#if:{{{label 7|{{{l7|}}}}}}|{{!}}{{{label 7|{{{l7}}}}}}}}]] }}{{#if:{{{8|}}}|{{#if:{{{9|}}}|, |,&nbsp;and }} [[:{{{8}}}{{#if:{{{label 8|{{{l8|}}}}}}|{{!}}{{{label 8|{{{l8}}}}}}}}]] }}{{#if:{{{9|}}}|{{#if:{{{10|}}}|, |,&nbsp;and }} [[:{{{9}}}{{#if:{{{label 9|{{{l9|}}}}}}|{{!}}{{{label 9|{{{l9}}}}}}}}]] }}{{#if:{{{10|}}}|{{#if:{{{11|}}}|, |,&nbsp;and }} [[:{{{10}}}{{#if:{{{label 10|{{{l10|}}}}}}|{{!}}{{{label 10|{{{l10}}}}}}}}]] }}{{#if:{{{11|}}}|{{#if:{{{12|}}}|, |,&nbsp;and }} [[:{{{11}}}{{#if:{{{label 11|{{{l11|}}}}}}|{{!}}{{{label 11|{{{l11}}}}}}}}]] }}{{#if:{{{12|}}}|{{#if:{{{13|}}}|, |,&nbsp;and }} [[:{{{12}}}{{#if:{{{label 12|{{{l12|}}}}}}|{{!}}{{{label 12|{{{l12}}}}}}}}]] }}{{#if:{{{13|}}}|{{#if:{{{14|}}}|, |,&nbsp;and }} [[:{{{13}}}{{#if:{{{label 13|{{{l13|}}}}}}|{{!}}{{{label 13|{{{l13}}}}}}}}]] }}{{#if:{{{14|}}}|{{#if:{{{15|}}}|, |,&nbsp;and }} [[:{{{14}}}{{#if:{{{label 14|{{{l14|}}}}}}|{{!}}{{{label 14|{{{l14}}}}}}}}]] }}{{#if:{{{15|}}}|,&nbsp;and [[:{{{15}}}{{#if:{{{label 15|{{{l15|}}} }}}|{{!}}{{{label 15|{{{l15|}}} }}} }}]] }}{{#if:{{{16|}}}| &mdash; '''<br/>Error: [[Template:See also|Too many links specified (maximum is 15)]]''' }}}}<noinclude> {{documentation}} </noinclude> 0315f43d7e4b679054955c7a50fe554ab1df63de Template:= 10 57 107 106 2023-08-28T02:15:12Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki =<noinclude> {{documentation}} </noinclude> 44f3105df6073eb65369938814d1551b51611402 Template:Para 10 58 109 108 2023-08-28T02:15:13Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <code class="tpl-para" style="word-break:break-word;{{SAFESUBST:<noinclude />#if:{{{plain|}}}|border: none; background-color: inherit;}} {{SAFESUBST:<noinclude />#if:{{{style|}}}|{{{style}}}}}">&#124;{{SAFESUBST:<noinclude />#if:{{{1|}}}|{{{1}}}&#61;}}{{{2|}}}</code><noinclude> {{Documentation}} <!--Categories and interwikis go near the bottom of the /doc subpage.--> </noinclude> 7be5bee75307eae9342bbb9ff3a613e93e93d5a7 Template:Ping 10 59 111 110 2023-08-28T02:15:13Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{{{{|safesubst:}}}#invoke:Reply to|replyto|<noinclude>example=Example</noinclude>|max=50}}<noinclude>{{documentation}}</noinclude> 0a7b3547181e17a03ec99855e276688fcc36ce1e Template:Abstain 10 60 113 112 2023-08-28T02:15:14Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki [[File:Symbol neutral vote.svg|18px]]&nbsp;'''<bdi>{{{1|Abstain}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> b1098b70832376165658562bfc8de5b6187bdb26 Template:Neutral 10 61 115 114 2023-08-28T02:15:15Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki [[File:Symbol neutral vote.svg|18px]]&nbsp;'''<bdi>{{{1|Neutral}}}</bdi>'''<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> 59552c46cb01ccf2c6196bdea9ec3eb90858e675 Template:Oppose 10 62 117 116 2023-08-28T02:15:15Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{ #switch: {{{4|{{{1|}}}}}} | Regular= [[File:Symbol oppose vote.png|18px|alt=]] | Normal= [[File:Symbol oppose vote.png|18px|alt=]] | Strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | Strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | Strongest = [[File:Symbol full oppose vote.svg|20px|alt=]] | Weak= [[File:Weak Oppose.png|18px|alt=]] | Weakly= [[File:Weak Oppose.png|18px|alt=]] | strongly= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | strong= [[File:Symbol oppose vote oversat.svg|18px|alt=]] | weak= [[File:Weak Oppose.png|18px|alt=]] | weakly= [[File:Weak Oppose.png|18px|alt=]] | strongest = [[File:Symbol full oppose vote.svg|20px|alt=]] |#default= [[File:Symbol oppose vote.svg|18px|alt=]] }} {{ #switch: {{{1|}}} | Regular='''Oppose''' | Normal= '''Oppose''' | Strongest = '''Strongest oppose''' | Strongly= '''Strongly oppose''' | Strong= '''Strong oppose''' | Weak= '''Weak oppose''' | Weakly= '''Weakly oppose''' | strongly= '''Strongly oppose''' | strong= '''Strong oppose''' | weak= '''Weak oppose''' | weakly= '''Weakly oppose''' | strongest = '''Strongest oppose''' | {{{other|2}}} = '''{{{3}}}''' |#default= '''Oppose''' }}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> 8c57115a1c36446a717d2c874b2895b057d1ffc3 Template:Support 10 63 119 118 2023-08-28T02:15:16Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{ #switch: {{{4|{{{1|}}}}}} | Regular= [[File:Symbol support vote.svg|18px|alt=]] | Normal= [[File:Symbol support vote.svg|18px|alt=]] | Strongly= [[File:Symbol strong support vote.svg|18px|alt=]] | Strongest= [[File:Symbol full support vote.svg|22px]] | Strong= [[File:Symbol strong support vote.svg|18px|alt=]] | Weak= [[File:Symbol partial support vote.svg|18px|alt=]] | Weakly= [[File:Symbol partial support vote.svg|18px|alt=]] | strongly= [[File:Symbol strong support vote.svg|18px|alt=]] | strong= [[File:Symbol strong support vote.svg|18px|alt=]] | weak= [[File:Symbol partial support vote.svg|18px|alt=]] | weakly= [[File:Symbol partial support vote.svg|18px|alt=]] |#default= [[File:Symbol support vote.svg|18px|alt=]] }} {{ #switch: {{{1|}}} | Regular='''Support''' | Normal= '''Support''' | Strongly= '''Strongly support''' | Strong= '''Strong support''' | Weak= '''Weak support''' | Weakly= '''Weakly support''' | strongly= '''Strongly support''' | Strongest= '''''Strongest support''''' | strong= '''Strong support''' | weak= '''Weak support''' | weakly= '''Weakly support''' | {{{other|2}}} = '''{{{3}}}''' |#default= '''Support''' }}<noinclude>{{documentation}}[[Category:Voting templates]]</noinclude> 0a43c05b804693f20b74446f7e7e6d7ccd10c516 Template:User github 10 64 121 120 2023-08-28T02:15:16Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{Userbox | id = [[File:GitLogo.png|43px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an account on GitHub{{#if:{{{account|}}}|&#32;as ''[[github:{{{account}}}|{{{account}}}]]''|}}. | nocat = {{{nocat|}}} | usercategory = Users who use GitHub }}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> 08ef531d5b5a1e69b84939e0fc1f1d0d622f38ad Template:User IRC 10 65 123 122 2023-08-28T02:15:17Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{Userbox | id = # | id-s = 24 | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:IRC|IRC]]{{#if:{{{nick|}}}|&#32;as ''{{{nick}}}''|}}. | nocat = {{{nocat|}}} | usercategory = Users who use IRC }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> a148152ff16bb6fc7f7a7bd46c4898b50f1996fc Template:User discord 10 66 125 124 2023-08-28T02:15:17Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{Userbox | id = # | id-s = 24 | id-fc = #5865F2 | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} chats on [[m:Discord|Discord]]{{#if:{{{account|}}}|&#32;as ''{{{account}}}''|}}. | nocat = {{{nocat|}}} | usercategory = Users who use Discord }}<noinclude>{{Documentation}}[[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> 19b1d90000718152b9058c16c7c1ba13d7cb2715 Template:User wikimedia 10 67 127 126 2023-08-28T02:15:18Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{Userbox | id = [[File:Wikimedia Foundation Logo.png|43px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an [[metawikimedia:Special:CentralAuth/{{{account|{{BASEPAGENAME}}}}}|account]] at the Wikimedia Foundation projects. | nocat = {{{nocat|}}} | usercategory = Wikimedians }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> c7b06f2b4d088ee94d893eb1e3548e9e0562fc5e Template:User youtube 10 68 129 128 2023-08-28T02:15:18Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{Userbox | id = [[File:YouTube full-color icon (2017).svg|37px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has a YouTube channel{{#if:{{{account|}}}|&#32;at [https://{{{account}}} '''{{{account}}}''']|}}. | nocat = {{{nocat|}}} | usercategory = Users who use YouTube }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> f0ba1080f2a2d69494317a9790fa3d7e6e4239b4 Template:User instagram 10 69 131 130 2023-08-28T02:15:18Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{Userbox | id = [[File:Instagram icon.png|37px]] | float = {{{float|right}}} | border-c = #808080 | id-c = #FFFFFF | info-c = #DBDBDB | info = {{#if:{{{username|}}}|''{{PAGENAME}}''|This user}} has an {{#if:{{{account|}}}|&#32;account [https://instagram.com/{{{account}}} '''@{{{account}}}''']|account}} on Instagram. | nocat = {{{nocat|}}} | usercategory = Users who use Instagram }}<noinclude>{{Documentation}} [[Category:Social media userboxes|{{PAGENAME}}]]</noinclude> 1178ad0721de804c08dd554ebb4b52c4c6569fde Template:Documentation subpage 10 70 133 132 2023-08-28T02:15:19Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <includeonly><!-- -->{{#ifeq:{{lc:{{SUBPAGENAME}}}} |{{{override|doc}}} | <!--(this template has been transcluded on a /doc or /{{{override}}} page)--> </includeonly><!-- -->{{#ifeq:{{{doc-notice|show}}} |show | {{Mbox | type = notice | style = margin-bottom:1.0em; | image = [[File:Edit-copy green.svg|40px|alt=|link=]] | text = '''This is a documentation subpage''' for '''{{{1|[[:{{SUBJECTSPACE}}:{{BASEPAGENAME}}]]}}}'''.<br/> It contains usage information, [[mw:Help:Categories|categories]] and other content that is not part of the original {{#if:{{{text2|}}} |{{{text2}}} |{{#if:{{{text1|}}} |{{{text1}}} | page}}}}. }} }}<!-- -->{{DEFAULTSORT:{{{defaultsort|{{PAGENAME}}}}}}}<!-- -->{{#if:{{{inhibit|}}} |<!--(don't categorize)--> | <includeonly><!-- -->{{#ifexist:{{NAMESPACE}}:{{BASEPAGENAME}} | [[Category:{{#switch:{{SUBJECTSPACE}} |Template=Template |Module=Module |User=User |#default=Wikipedia}} documentation pages]] | [[Category:Documentation subpages without corresponding pages]] }}<!-- --></includeonly> }}<!-- (completing initial #ifeq: at start of template:) --><includeonly> | <!--(this template has not been transcluded on a /doc or /{{{override}}} page)--> }}<!-- --></includeonly><noinclude>{{Documentation}}</noinclude> 471e685c1c643a5c6272e20e49824fffebad0448 Template:Mbox 10 71 135 134 2023-08-28T02:15:19Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{#invoke:Message box|mbox}}<noinclude> {{documentation}} <!-- Categories go on the /doc subpage, and interwikis go on Wikidata. --> </noinclude> c262e205f85f774a23f74119179ceea11751d68e Template:MessageBox 10 72 137 136 2023-08-28T02:15:20Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <div style="width: {{#if:{{{width|}}}|{{{width}}}|80%}}; background-color: {{#if:{{{Background color}}}|{{{Background color}}}|#f5f5f5}}; border-top: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-bottom: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-right: 1px solid {{#if:{{{Border color}}}|{{{Border color}}}|#aaaaaa}}; border-left: 12px solid {{#if:{{{Flag color}}}|{{{Flag color}}}|#aaaaaa}}; margin: 0.5em auto 0.5em;"> {| {{#if:{{{Image}}}|{{!}}style="width:93px; text-align:center; vertical-align:middle; padding-top:1px;padding-bottom:7px" {{!}} {{{Image}}} }} |style="vertical-align:middle;padding-left:3px;padding-top:10px;padding-bottom:10px;padding-right:10px; background-color: {{#if:{{{Background color}}}{{!}}{{{Background color}}}{{!}}#f5f5f5}};" | {{{Message text}}} |} </div><noinclude>[[Category:Notice templates]]</noinclude> c6727bf6179a36a5413ed93f232fd0e2f7180256 Template:Shortcut 10 73 139 138 2023-08-28T02:15:20Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <!-- Putting anchors on page: --><div style="position: relative; top: -3em;">{{#if:{{{1|}}}|<span id="{{anchorencode:{{{1|}}}}}"></span> }}{{#if:{{{2|}}}|<span id="{{anchorencode:{{{2|}}}}}"></span> }}{{#if:{{{3|}}}|<span id="{{anchorencode:{{{3|}}}}}"></span> }}{{#if:{{{4|}}}|<span id="{{anchorencode:{{{4|}}}}}"></span> }}{{#if:{{{5|}}}|<span id="{{anchorencode:{{{5|}}}}}"></span> }}</div> <table class="shortcutbox noprint" style="float: right; border: 1px solid #aaa; background: #fff; margin: .3em .3em .3em 1em; padding: 3px; text-align: center;"><tr><th style="border: none; background: transparent;" class="plainlist"><!-- Adding the shortcut links: --><small>[[w:Wikipedia:Shortcut|Shortcut{{#if:{{{2|}}}|s}}]]: {{#if:{{{1|}}}|<ul><li> [[{{{1}}}]]</li> }}{{#if:{{{2|}}}|<li> [[{{{2}}}]]</li> }}{{#if:{{{3|}}}|<li> [[{{{3}}}]]</li> }}{{#if:{{{4|}}}|<li> [[{{{4}}}]]</li> }}{{#if:{{{5|}}}|<li> [[{{{5}}}]]</li> }}{{#if:{{{msg|}}}|<li> {{{msg}}}</li> }}</ul></small></th></tr></table><noinclude>{{doc}}</noinclude> 96a4c79718f5cbdf3e074cc545193ea4e863d1fb Template:Template link 10 74 141 140 2023-08-28T02:15:21Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki &#123;&#123;[[Template:{{{1}}}|{{{1}}}]]&#125;&#125;<noinclude>{{documentation}} <!-- Categories go on the /doc subpage and interwikis go on Wikidata. --> </noinclude> eabbec62efe3044a98ebb3ce9e7d4d43c222351d Template:Template link expanded 10 75 143 142 2023-08-28T02:15:21Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <code><nowiki>{{</nowiki>{{#if:{{{subst|}}} |[[Help:Substitution|subst]]:}}<!-- -->[[{{{sister|{{{SISTER|}}}}}}{{ns:Template}}:{{{1|}}}|{{{1|}}}]]<!-- -->{{#if:{{{2|}}} |&#124;{{{2}}}}}<!-- -->{{#if:{{{3|}}} |&#124;{{{3}}}}}<!-- -->{{#if:{{{4|}}} |&#124;{{{4}}}}}<!-- -->{{#if:{{{5|}}} |&#124;{{{5}}}}}<!-- -->{{#if:{{{6|}}} |&#124;{{{6}}}}}<!-- -->{{#if:{{{7|}}} |&#124;{{{7}}}}}<!-- -->{{#if:{{{8|}}} |&#124;{{{8}}}}}<!-- -->{{#if:{{{9|}}} |&#124;{{{9}}}}}<!-- -->{{#if:{{{10|}}} |&#124;{{{10}}}}}<!-- -->{{#if:{{{11|}}} |&#124;{{{11}}}}}<!-- -->{{#if:{{{12|}}} |&#124;{{{12}}}}}<!-- -->{{#if:{{{13|}}} |&#124;{{{13}}}}}<!-- -->{{#if:{{{14|}}} |&#124;{{{14}}}}}<!-- -->{{#if:{{{15|}}} |&#124;{{{15}}}}}<!-- -->{{#if:{{{16|}}} |&#124;{{{16}}}}}<!-- -->{{#if:{{{17|}}} |&#124;{{{17}}}}}<!-- -->{{#if:{{{18|}}} |&#124;{{{18}}}}}<!-- -->{{#if:{{{19|}}} |&#124;{{{19}}}}}<!-- -->{{#if:{{{20|}}} |&#124;{{{20}}}}}<!-- -->{{#if:{{{21|}}} |&#124;''...''}}<!-- --><nowiki>}}</nowiki></code><noinclude> {{Documentation}} </noinclude> 9f670205d4b358df089b1a820f78f02a88afca3a Template:Discussion top 10 76 145 144 2023-08-28T02:15:21Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <div class="boilerplate metadata discussion-archived" style="background-color: #F2F4FC; margin: 2em 0 0 0; padding: 0 10px 0 10px; border: 1px solid #aaa"> :The following discussion is closed. Please do not modify it. Subsequent comments should be made in a new section. ::{{{1|}}} ----<noinclude></div>{{documentation}}</noinclude> c8b38525e188dbfa68b0e9cdd1864ceff2ed100e Template:Discussion bottom 10 77 147 146 2023-08-28T02:15:22Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <noinclude><div></noinclude>---- :The above discussion is preserved as an archive. Please do not modify it. Subsequent comments should be made in a new section </div><noinclude>{{documentation|Template:Discussion top/doc}}</noinclude> 80d5baa979985b3b685585611b0e954d2c1c6e10 Template:Current time 10 78 149 148 2023-08-28T02:15:22Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{#switch:{{{1}}} |Coordinated Universal Time=Current UTC is {{CURRENTTIME}} |UTC-1=Current time for {{{1}}} is {{utc|23}} |UTC-2=Current time for {{{1}}} is {{utc|22}} |UTC-2:30=Current time for {{{1}}} is {{utc|21|30}} |UTC-3=Current time for {{{1}}} is {{utc|21}} |UTC-3:30=Current time for {{{1}}} is {{utc|20|30}} |UTC-4=Current time for {{{1}}} is {{utc|20}} |UTC-5=Current time for {{{1}}} is {{utc|19}} |UTC-6=Current time for {{{1}}} is {{utc|18}} |UTC-7=Current time for {{{1}}} is {{utc|17}} |UTC-8=Current time for {{{1}}} is {{utc|16}} |UTC-9=Current time for {{{1}}} is {{utc|15}} |UTC-9:30=Current time for {{{1}}} is {{utc|14|30}} |UTC-10=Current time for {{{1}}} is {{utc|14}} |UTC-11=Current time for {{{1}}} is {{utc|13}} |UTC-12=Current time for {{{1}}} is {{utc|12}} |UTC+0:20=Current time for {{{1}}} is {{utc|0|20}} |UTC+0:30=Current time for {{{1}}} is {{utc|0|30}} |UTC+1=Current time for {{{1}}} is {{utc|1}} |UTC+2=Current time for {{{1}}} is {{utc|2}} |UTC+3=Current time for {{{1}}} is {{utc|3}} |UTC+3:30=Current time for {{{1}}} is {{utc|3|30}} |UTC+4=Current time for {{{1}}} is {{utc|4}} |UTC+4:30=Current time for {{{1}}} is {{utc|4|30}} |UTC+4:51=Current time for {{{1}}} is {{utc|4|51}} |UTC+5=Current time for {{{1}}} is {{utc|5}} |UTC+5:30=Current time for {{{1}}} is {{utc|5|30}} |UTC+5:40=Current time for {{{1}}} is {{utc|5|40}} |UTC+5:45=Current time for {{{1}}} is {{utc|5|45}} |UTC+6=Current time for {{{1}}} is {{utc|6}} |UTC+6:30=Current time for {{{1}}} is {{utc|6|30}} |UTC+7=Current time for {{{1}}} is {{utc|7}} |UTC+7:20=Current time for {{{1}}} is {{utc|7|20}} |UTC+7:30=Current time for {{{1}}} is {{utc|7|30}} |UTC+8=Current time for {{{1}}} is {{utc|8}} |UTC+8:30=Current time for {{{1}}} is {{utc|8|30}} |UTC+8:45=Current time for {{{1}}} is {{utc|8|45}} |UTC+9=Current time for {{{1}}} is {{utc|9}} |UTC+9:30=Current time for {{{1}}} is {{utc|9|30}} |UTC+10=Current time for {{{1}}} is {{utc|10}} |UTC+10:30=Current time for {{{1}}} is {{utc|10|30}} |UTC+11=Current time for {{{1}}} is {{utc|11}} |UTC+11:30=Current time for {{{1}}} is {{utc|11|30}} |UTC+12=Current time for {{{1}}} is {{utc|12}} |UTC+12:45=Current time for {{{1}}} is {{utc|12|45}} |UTC+13=Current time for {{{1}}} is {{utc|13}} |UTC+13:45=Current time for {{{1}}} is {{utc|13|45}} |UTC+14=Current time for {{{1}}} is {{utc|14}} |#default=Current time is {{CURRENTTIME}} }}<noinclude>{{documentation|content=Returns the current time in a given timezone (defaulting to the timezone specified in [[Special:ManageWiki/settings#mw-section-localisation]], which in turn defaults to UTC) == Examples == {{tlx|current time}} -> {{current time}} {{tlx|current time|UTC+1}} -> {{current time|UTC+1}} {{tlx|current time|UTC-5}} -> {{current time|UTC-5}} [[Category:Templates]] }}</noinclude> 84d7f12dbea154240f9fa86372863cd6152dd98b Template:Utc 10 79 151 150 2023-08-28T02:15:22Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki {{#time:H:i|{{#expr:{{{1|0}}} * 60 + {{{2|0}}} round 0}} minutes}}<noinclude> {{documentation}} <!-- PLEASE ADD CATEGORIES AND INTERWIKIS TO THE /doc SUBPAGE, THANKS --> </noinclude> d24309676bfe4692d038657a7171952e7d1cded7 Template:Hatnote 10 80 153 152 2023-08-28T02:15:23Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <div style="margin-left:2em; margin-right: 2em;>''{{{1}}}''</div> <!-- The wikipedia templates uses :, which generated dd and dt tags. That is not ideal for accessibility. --><noinclude> This is a general purpose template for all kind of [https://en.wikipedia.org/wiki/Wikipedia:Hatnote hatnotes]. '''Hatnotes''' are a small annotation above a page or a section that can help the reader navigate. It's used for example to clarify what a section is about and link to other pages the reader may want to read. == Example usage == <pre><nowiki> === Heading === {{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}} The first sentence of the section is here. </nowiki></pre> === Heading === {{hatnote|This section is about headings in text, for the human body part see [[Head|Head]]. <br> For the the first level heading see the main article [[Headline]].</br> H1 redirects here, for the car see [[Hyundai#H1|Hyundai H1]].}} The first sentence of the section is here. <templatedata> { "params": { "1": { "label": "content", "description": "the content of the note", "example": "For the xxx see [[yyy]]." } }, "description": "Adds a annotation for the reader about the content that follows. Usually used after a heading." } </templatedata> [[Category:Templates]] </noinclude> 5e58f83d6fa53ed06f85139184aff1d651804efe Template:Description missing 10 81 155 154 2023-08-28T02:15:23Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <div class="boilerplate metadata" id="cleanup" style="text-align: center; background: #ffe; margin: .75em 15%; padding: .5em; border: 1px solid #e3e3b0;"> This media has no '''{{ #if: {{{1|}}} | {{{1}}} | description }}''', and may be lacking other information. <br> Media should have a summary to inform others of the content, author, source, and date if possible. If you know or have access to such information, please add it to the image page. </div> <includeonly>{{#switch:{{NAMESPACE}}|{{ns:6}}=|#default={{#ifeq:{{{category|}}}|no||[[Category:Images lacking a description|{{PAGENAME}}]]}}}}</includeonly><noinclude> {{documentation}} </noinclude> 2b5026cefd37c307f7f2ee331289c38741f834a5 MediaWiki:Gadget-morebits.js 8 82 157 156 2023-08-28T02:15:24Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 javascript text/javascript // <nowiki> /** * morebits.js * =========== * A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia. * * The highlights include: * - Morebits.quickForm class - generates quick HTML forms on the fly * - Morebits.wiki.api class - makes calls to the MediaWiki API * - Morebits.wiki.page class - modifies pages on the wiki (edit, revert, delete, etc.) * - Morebits.wikitext class - contains some utilities for dealing with wikitext * - Morebits.status class - a rough-and-ready status message displayer, used by the Morebits.wiki classes * - Morebits.simpleWindow class - a wrapper for jQuery UI Dialog with a custom look and extra features * * Dependencies: * - The whole thing relies on jQuery. But most wikis should provide this by default. * - Morebits.quickForm, Morebits.simpleWindow, and Morebits.status rely on the "morebits.css" file for their styling. * - Morebits.simpleWindow relies on jquery UI Dialog (ResourceLoader module name 'jquery.ui'). * - Morebits.quickForm tooltips rely on Tipsy (ResourceLoader module name 'jquery.tipsy'). * For external installations, Tipsy is available at [http://onehackoranother.com/projects/jquery/tipsy]. * - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition: * * GadgetName[ResourceLoader|dependencies=mediawiki.util,jquery.ui,jquery.tipsy]|morebits.js|morebits.css|GadgetName.js * * Most of the stuff here doesn't work on IE < 9. It is your script's responsibility to enforce this. * * This library is maintained by the maintainers of Twinkle. * For queries, suggestions, help, etc., head to [[Wikipedia talk:Twinkle]] on English Wikipedia [http://en.wikipedia.org]. * The latest development source is available at [https://github.com/azatoth/twinkle/blob/master/morebits.js]. * * From simplewiki */ ( function ( window, document, $, undefined ) { // Wrap entire file with anonymous function var Morebits = {}; window.Morebits = Morebits; // allow global access /** * **************** Morebits.userIsInGroup() **************** * Simple helper function to see what groups a user might belong */ Morebits.userIsInGroup = function ( group ) { return $.inArray(group, mw.config.get( 'wgUserGroups' )) !== -1; } /** * **************** Morebits.isIPAddress() **************** * Helper function: Returns true if given string contains a valid IPv4 or * IPv6 address */ Morebits.isIPAddress = function ( address ) { return mw.util.isIPv4Address(address) || mw.util.isIPv6Address(address); }; /** * **************** Morebits.sanitizeIPv6() **************** * JavaScript translation of the MediaWiki core function IP::sanitizeIP() in * includes/utils/IP.php. * Converts an IPv6 address to the canonical form stored and used by MediaWiki. */ Morebits.sanitizeIPv6 = function ( address ) { address = address.trim(); if ( address === '' ) { return null; } if ( mw.util.isIPv4Address( address ) || !mw.util.isIPv6Address( address ) ) { return address; // nothing else to do for IPv4 addresses or invalid ones } // Remove any whitespaces, convert to upper case address = address.toUpperCase(); // Expand zero abbreviations var abbrevPos = address.indexOf( '::' ); if ( abbrevPos > -1 ) { // We know this is valid IPv6. Find the last index of the // address before any CIDR number (e.g. "a:b:c::/24"). var CIDRStart = address.indexOf( '/' ); var addressEnd = ( CIDRStart > -1 ) ? CIDRStart - 1 : address.length - 1; // If the '::' is at the beginning... var repeat, extra, pad; if ( abbrevPos === 0 ) { repeat = '0:'; extra = ( address == '::' ) ? '0' : ''; // for the address '::' pad = 9; // 7+2 (due to '::') // If the '::' is at the end... } else if ( abbrevPos === ( addressEnd - 1 ) ) { repeat = ':0'; extra = ''; pad = 9; // 7+2 (due to '::') // If the '::' is in the middle... } else { repeat = ':0'; extra = ':'; pad = 8; // 6+2 (due to '::') } var replacement = repeat; pad -= address.split( ':' ).length - 1; for ( var i = 1; i < pad; i++ ) { replacement += repeat; } replacement += extra; address = address.replace( '::', replacement ); } // Remove leading zeros from each bloc as needed address = address.replace( /(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2' ); return address; }; /** * **************** Morebits.quickForm **************** * Morebits.quickForm is a class for creation of simple and standard forms without much * specific coding. * * Index to Morebits.quickForm element types: * * select A combo box (aka drop-down). * - Attributes: name, label, multiple, size, list, event * option An element for a combo box. * - Attributes: value, label, selected, disabled * optgroup A group of "option"s. * - Attributes: label, list * field A fieldset (aka group box). * - Attributes: name, label * checkbox A checkbox. Must use "list" parameter. * - Attributes: name, list, event * - Attributes (within list): name, label, value, checked, disabled, event, subgroup * radio A radio button. Must use "list" parameter. * - Attributes: name, list, event * - Attributes (within list): name, label, value, checked, disabled, event, subgroup * input A text box. * - Attributes: name, label, value, size, disabled, readonly, maxlength, event * dyninput A set of text boxes with "Remove" buttons and an "Add" button. * - Attributes: name, label, min, max, sublabel, value, size, maxlength, event * hidden An invisible form field. * - Attributes: name, value * header A level 5 header. * - Attributes: label * div A generic placeholder element or label. * - Attributes: name, label * submit A submit button. Morebits.simpleWindow moves these to the footer of the dialog. * - Attributes: name, label, disabled * button A generic button. * - Attributes: name, label, disabled, event * textarea A big, multi-line text box. * - Attributes: name, label, value, cols, rows, disabled, readonly * * Global attributes: id, style, tooltip, extra, adminonly */ Morebits.quickForm = function QuickForm( event, eventType ) { this.root = new Morebits.quickForm.element( { type: 'form', event: event, eventType:eventType } ); }; Morebits.quickForm.prototype.render = function QuickFormRender() { var ret = this.root.render(); ret.names = {}; return ret; }; Morebits.quickForm.prototype.append = function QuickFormAppend( data ) { return this.root.append( data ); }; Morebits.quickForm.element = function QuickFormElement( data ) { this.data = data; this.childs = []; this.id = Morebits.quickForm.element.id++; }; Morebits.quickForm.element.id = 0; Morebits.quickForm.element.prototype.append = function QuickFormElementAppend( data ) { var child; if( data instanceof Morebits.quickForm.element ) { child = data; } else { child = new Morebits.quickForm.element( data ); } this.childs.push( child ); return child; }; // This should be called without parameters: form.render() Morebits.quickForm.element.prototype.render = function QuickFormElementRender( internal_subgroup_id ) { var currentNode = this.compute( this.data, internal_subgroup_id ); for( var i = 0; i < this.childs.length; ++i ) { // do not pass internal_subgroup_id to recursive calls currentNode[1].appendChild( this.childs[i].render() ); } return currentNode[0]; }; Morebits.quickForm.element.prototype.compute = function QuickFormElementCompute( data, in_id ) { var node; var childContainder = null; var label; var id = ( in_id ? in_id + '_' : '' ) + 'node_' + this.id; if( data.adminonly && !Morebits.userIsInGroup( 'sysop' ) ) { // hell hack alpha data.type = 'hidden'; } var i, current, subnode; switch( data.type ) { case 'form': node = document.createElement( 'form' ); node.className = "quickform"; node.setAttribute( 'action', 'javascript:void(0);'); if( data.event ) { node.addEventListener( data.eventType || 'submit', data.event , false ); } break; case 'select': node = document.createElement( 'div' ); node.setAttribute( 'id', 'div_' + id ); if( data.label ) { label = node.appendChild( document.createElement( 'label' ) ); label.setAttribute( 'for', id ); label.appendChild( document.createTextNode( data.label ) ); } var select = node.appendChild( document.createElement( 'select' ) ); if( data.event ) { select.addEventListener( 'change', data.event, false ); } if( data.multiple ) { select.setAttribute( 'multiple', 'multiple' ); } if( data.size ) { select.setAttribute( 'size', data.size ); } select.setAttribute( 'name', data.name ); if( data.list ) { for( i = 0; i < data.list.length; ++i ) { current = data.list[i]; if( current.list ) { current.type = 'optgroup'; } else { current.type = 'option'; } subnode = this.compute( current ); select.appendChild( subnode[0] ); } } childContainder = select; break; case 'option': node = document.createElement( 'option' ); node.values = data.value; node.setAttribute( 'value', data.value ); if( data.selected ) { node.setAttribute( 'selected', 'selected' ); } if( data.disabled ) { node.setAttribute( 'disabled', 'disabled' ); } node.setAttribute( 'label', data.label ); node.appendChild( document.createTextNode( data.label ) ); break; case 'optgroup': node = document.createElement( 'optgroup' ); node.setAttribute( 'label', data.label ); if( data.list ) { for( i = 0; i < data.list.length; ++i ) { current = data.list[i]; current.type = 'option'; //must be options here subnode = this.compute( current ); node.appendChild( subnode[0] ); } } break; case 'field': node = document.createElement( 'fieldset' ); label = node.appendChild( document.createElement( 'legend' ) ); label.appendChild( document.createTextNode( data.label ) ); if( data.name ) { node.setAttribute( 'name', data.name ); } break; case 'checkbox': case 'radio': node = document.createElement( 'div' ); if( data.list ) { for( i = 0; i < data.list.length; ++i ) { var cur_id = id + '_' + i; current = data.list[i]; var cur_div; if( current.type === 'header' ) { // inline hack cur_div = node.appendChild( document.createElement( 'h6' ) ); cur_div.appendChild( document.createTextNode( current.label ) ); if( current.tooltip ) { Morebits.quickForm.element.generateTooltip( cur_div , current ); } continue; } cur_div = node.appendChild( document.createElement( 'div' ) ); subnode = cur_div.appendChild( document.createElement( 'input' ) ); subnode.values = current.value; subnode.setAttribute( 'value', current.value ); subnode.setAttribute( 'name', current.name || data.name ); subnode.setAttribute( 'type', data.type ); subnode.setAttribute( 'id', cur_id ); if( current.checked ) { subnode.setAttribute( 'checked', 'checked' ); } if( current.disabled ) { subnode.setAttribute( 'disabled', 'disabled' ); } if( data.event ) { subnode.addEventListener( 'change', data.event, false ); } else if ( current.event ) { subnode.addEventListener( 'change', current.event, true ); } label = cur_div.appendChild( document.createElement( 'label' ) ); label.appendChild( document.createTextNode( current.label ) ); label.setAttribute( 'for', cur_id ); if( current.tooltip ) { Morebits.quickForm.element.generateTooltip( label, current ); } var event; if( current.subgroup ) { var tmpgroup = current.subgroup; // $.extend({}, current.subgroup); really needed? if( ! $.isArray( tmpgroup ) ) { tmpgroup = [ tmpgroup ]; } var subgroupRaw = new Morebits.quickForm.element({ type: 'div', id: id + '_' + i + '_subgroup' }); $.each( tmpgroup, function( idx, el ) { if( ! el.type ) { el.type = data.type; } el.name = (current.name || data.name) + '.' + el.name; subgroupRaw.append( el ); } ); var subgroup = subgroupRaw.render( cur_id ); subgroup.className = "quickformSubgroup"; subnode.subgroup = subgroup; subnode.shown = false; event = function(e) { if( e.target.checked ) { e.target.parentNode.appendChild( e.target.subgroup ); if( e.target.type === 'radio' ) { var name = e.target.name; if( e.target.form.names[name] !== undefined ) { e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup ); } e.target.form.names[name] = e.target; } } else { e.target.parentNode.removeChild( e.target.subgroup ); } }; subnode.addEventListener( 'change', event, true ); if( current.checked ) { subnode.parentNode.appendChild( subgroup ); } } else if( data.type === 'radio' ) { event = function(e) { if( e.target.checked ) { var name = e.target.name; if( e.target.form.names[name] !== undefined ) { e.target.form.names[name].parentNode.removeChild( e.target.form.names[name].subgroup ); } delete e.target.form.names[name]; } }; subnode.addEventListener( 'change', event, true ); } } } break; case 'input': node = document.createElement( 'div' ); node.setAttribute( 'id', 'div_' + id ); if( data.label ) { label = node.appendChild( document.createElement( 'label' ) ); label.appendChild( document.createTextNode( data.label ) ); label.setAttribute( 'for', id ); } subnode = node.appendChild( document.createElement( 'input' ) ); if( data.value ) { subnode.setAttribute( 'value', data.value ); } subnode.setAttribute( 'name', data.name ); subnode.setAttribute( 'id', id ); subnode.setAttribute( 'type', 'text' ); if( data.size ) { subnode.setAttribute( 'size', data.size ); } if( data.disabled ) { subnode.setAttribute( 'disabled', 'disabled' ); } if( data.readonly ) { subnode.setAttribute( 'readonly', 'readonly' ); } if( data.maxlength ) { subnode.setAttribute( 'maxlength', data.maxlength ); } if( data.event ) { subnode.addEventListener( 'keyup', data.event, false ); } break; case 'dyninput': var min = data.min || 1; var max = data.max || Infinity; node = document.createElement( 'div' ); label = node.appendChild( document.createElement( 'h5' ) ); label.appendChild( document.createTextNode( data.label ) ); var listNode = node.appendChild( document.createElement( 'div' ) ); var more = this.compute( { type: 'button', label: 'more', disabled: min >= max, event: function(e) { var area = e.target.area; var new_node = new Morebits.quickForm.element( e.target.sublist ); e.target.area.appendChild( new_node.render() ); if( ++e.target.counter >= e.target.max ) { e.target.setAttribute( 'disabled', 'disabled' ); } e.stopPropagation(); } } ); node.appendChild( more[0] ); var moreButton = more[1]; var sublist = { type: '_dyninput_element', label: data.sublabel || data.label, name: data.name, value: data.value, size: data.size, remove: false, maxlength: data.maxlength, event: data.event }; for( i = 0; i < min; ++i ) { var elem = new Morebits.quickForm.element( sublist ); listNode.appendChild( elem.render() ); } sublist.remove = true; sublist.morebutton = moreButton; sublist.listnode = listNode; moreButton.sublist = sublist; moreButton.area = listNode; moreButton.max = max - min; moreButton.counter = 0; break; case '_dyninput_element': // Private, similar to normal input node = document.createElement( 'div' ); if( data.label ) { label = node.appendChild( document.createElement( 'label' ) ); label.appendChild( document.createTextNode( data.label ) ); label.setAttribute( 'for', id ); } subnode = node.appendChild( document.createElement( 'input' ) ); if( data.value ) { subnode.setAttribute( 'value', data.value ); } subnode.setAttribute( 'name', data.name ); subnode.setAttribute( 'type', 'text' ); if( data.size ) { subnode.setAttribute( 'size', data.size ); } if( data.maxlength ) { subnode.setAttribute( 'maxlength', data.maxlength ); } if( data.event ) { subnode.addEventListener( 'keyup', data.event, false ); } if( data.remove ) { var remove = this.compute( { type: 'button', label: 'remove', event: function(e) { var list = e.target.listnode; var node = e.target.inputnode; var more = e.target.morebutton; list.removeChild( node ); --more.counter; more.removeAttribute( 'disabled' ); e.stopPropagation(); } } ); node.appendChild( remove[0] ); var removeButton = remove[1]; removeButton.inputnode = node; removeButton.listnode = data.listnode; removeButton.morebutton = data.morebutton; } break; case 'hidden': node = document.createElement( 'input' ); node.setAttribute( 'type', 'hidden' ); node.values = data.value; node.setAttribute( 'value', data.value ); node.setAttribute( 'name', data.name ); break; case 'header': node = document.createElement( 'h5' ); node.appendChild( document.createTextNode( data.label ) ); break; case 'div': node = document.createElement( 'div' ); if (data.name) { node.setAttribute( 'name', data.name ); } if (data.label) { if ( ! $.isArray( data.label ) ) { data.label = [ data.label ]; } var result = document.createElement( 'span' ); result.className = 'quickformDescription'; for( i = 0; i < data.label.length; ++i ) { if( typeof data.label[i] === 'string' ) { result.appendChild( document.createTextNode( data.label[i] ) ); } else if( data.label[i] instanceof Element ) { result.appendChild( data.label[i] ); } } node.appendChild( result ); } break; case 'submit': node = document.createElement( 'span' ); childContainder = node.appendChild(document.createElement( 'input' )); childContainder.setAttribute( 'type', 'submit' ); if( data.label ) { childContainder.setAttribute( 'value', data.label ); } childContainder.setAttribute( 'name', data.name || 'submit' ); if( data.disabled ) { childContainder.setAttribute( 'disabled', 'disabled' ); } break; case 'button': node = document.createElement( 'span' ); childContainder = node.appendChild(document.createElement( 'input' )); childContainder.setAttribute( 'type', 'button' ); if( data.label ) { childContainder.setAttribute( 'value', data.label ); } childContainder.setAttribute( 'name', data.name ); if( data.disabled ) { childContainder.setAttribute( 'disabled', 'disabled' ); } if( data.event ) { childContainder.addEventListener( 'click', data.event, false ); } break; case 'textarea': node = document.createElement( 'div' ); node.setAttribute( 'id', 'div_' + id ); if( data.label ) { label = node.appendChild( document.createElement( 'h5' ) ); label.appendChild( document.createTextNode( data.label ) ); // TODO need to nest a <label> tag in here without creating extra vertical space //label.setAttribute( 'for', id ); } subnode = node.appendChild( document.createElement( 'textarea' ) ); subnode.setAttribute( 'name', data.name ); if( data.cols ) { subnode.setAttribute( 'cols', data.cols ); } if( data.rows ) { subnode.setAttribute( 'rows', data.rows ); } if( data.disabled ) { subnode.setAttribute( 'disabled', 'disabled' ); } if( data.readonly ) { subnode.setAttribute( 'readonly', 'readonly' ); } if( data.value ) { subnode.value = data.value; } break; default: throw new Error("Morebits.quickForm: unknown element type " + data.type.toString()); } if( !childContainder ) { childContainder = node; } if( data.tooltip ) { Morebits.quickForm.element.generateTooltip( label || node , data ); } if( data.extra ) { childContainder.extra = data.extra; } if( data.style ) { childContainder.setAttribute( 'style', data.style ); } childContainder.setAttribute( 'id', data.id || id ); return [ node, childContainder ]; }; Morebits.quickForm.element.generateTooltip = function QuickFormElementGenerateTooltip( node, data ) { $('<span/>', { 'class': 'ui-icon ui-icon-help ui-icon-inline morebits-tooltip' }).appendTo(node).tipsy({ 'fallback': data.tooltip, 'fade': true, 'gravity': $.fn.tipsy.autoWE, 'html': true, 'delayOut': 250 }); }; /** * Some utility methods for manipulating quickForms after their creation * (None of them work for "dyninput" type fields at present) * * Morebits.quickForm.getElements(form, fieldName) * Returns all form elements with a given field name or ID * * Morebits.quickForm.getCheckboxOrRadio(elementArray, value) * Searches the array of elements for a checkbox or radio button with a certain |value| attribute * * Morebits.quickForm.getElementContainer(element) * Returns the <div> containing the form element, or the form element itself * May not work as expected on checkboxes or radios * * Morebits.quickForm.getElementLabelObject(element) * Gets the HTML element that contains the label of the given form element (mainly for internal use) * * Morebits.quickForm.getElementLabel(element) * Gets the label text of the element * * Morebits.quickForm.setElementLabel(element, labelText) * Sets the label of the element to the given text * * Morebits.quickForm.overrideElementLabel(element, temporaryLabelText) * Stores the element's current label, and temporarily sets the label to the given text * * Morebits.quickForm.resetElementLabel(element) * Restores the label stored by overrideElementLabel * * Morebits.quickForm.setElementVisibility(element, visibility) * Shows or hides a form element plus its label and tooltip * * Morebits.quickForm.setElementTooltipVisibility(element, visibility) * Shows or hides the "question mark" icon next to a form element */ Morebits.quickForm.getElements = function QuickFormGetElements(form, fieldName) { var $form = $(form); var $elements = $form.find('[name="' + fieldName + '"]'); if ($elements.length > 0) { return $elements.toArray(); } $elements = $form.find('#' + fieldName); if ($elements.length > 0) { return $elements.toArray(); } return null; }; Morebits.quickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) { var found = $.grep(elementArray, function(el) { return el.value === value; }); if (found.length > 0) { return found[0]; } return null; }; Morebits.quickForm.getElementContainer = function QuickFormGetElementContainer(element) { // for divs, headings and fieldsets, the container is the element itself if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) { return element; } // for others, just return the parent node return element.parentNode; }; Morebits.quickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) { // for buttons, divs and headers, the label is on the element itself if (element.type === "button" || element.type === "submit" || element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) { return element; // for fieldsets, the label is the child <legend> element } else if (element instanceof HTMLFieldSetElement) { return element.getElementsByTagName("legend")[0]; // for textareas, the label is the sibling <h5> element } else if (element instanceof HTMLTextAreaElement) { return element.parentNode.getElementsByTagName("h5")[0]; // for others, the label is the sibling <label> element } else { return element.parentNode.getElementsByTagName("label")[0]; } return null; }; Morebits.quickForm.getElementLabel = function QuickFormGetElementLabel(element) { var labelElement = Morebits.quickForm.getElementLabelObject(element); if (!labelElement) { return null; } return labelElement.firstChild.textContent; }; Morebits.quickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) { var labelElement = Morebits.quickForm.getElementLabelObject(element); if (!labelElement) { return false; } labelElement.firstChild.textContent = labelText; return true; }; Morebits.quickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) { if (!element.hasAttribute("data-oldlabel")) { element.setAttribute("data-oldlabel", Morebits.quickForm.getElementLabel(element)); } return Morebits.quickForm.setElementLabel(element, temporaryLabelText); }; Morebits.quickForm.resetElementLabel = function QuickFormResetElementLabel(element) { if (element.hasAttribute("data-oldlabel")) { return Morebits.quickForm.setElementLabel(element, element.getAttribute("data-oldlabel")); } return null; }; Morebits.quickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) { $(element).toggle(visibility); }; Morebits.quickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) { $(Morebits.quickForm.getElementContainer(element)).find(".morebits-tooltip").toggle(visibility); }; /** * **************** HTMLFormElement **************** * * getChecked: * XXX Doesn't seem to work reliably across all browsers at the moment. -- see getChecked2 in twinkleunlink.js, which is better * * Returns an array containing the values of elements with the given name, that has it's * checked property set to true. (i.e. a checkbox or a radiobutton is checked), or select options * that have selected set to true. (don't try to mix selects with radio/checkboxes, please) * Type is optional and can specify if either radio or checkbox (for the event * that both checkboxes and radiobuttons have the same name. */ HTMLFormElement.prototype.getChecked = function( name, type ) { var elements = this.elements[name]; if( !elements ) { // if the element doesn't exists, return null. return null; } var return_array = []; var i; if( elements instanceof HTMLSelectElement ) { var options = elements.options; for( i = 0; i < options.length; ++i ) { if( options[i].selected ) { if( options[i].values ) { return_array.push( options[i].values ); } else { return_array.push( options[i].value ); } } } } else if( elements instanceof HTMLInputElement ) { if( type && elements.type !== type ) { return []; } else if( elements.checked ) { return [ elements.value ]; } } else { for( i = 0; i < elements.length; ++i ) { if( elements[i].checked ) { if( type && elements[i].type !== type ) { continue; } if( elements[i].values ) { return_array.push( elements[i].values ); } else { return_array.push( elements[i].value ); } } } } return return_array; }; /** * **************** RegExp **************** * * RegExp.escape: Will escape a string to be used in a RegExp */ RegExp.escape = function( text, space_fix ) { text = mw.RegExp.escape(text); // Special MediaWiki escape - underscore/space are often equivalent if( space_fix ) { text = text.replace( / |_/g, '[_ ]' ); } return text; }; /** * **************** Morebits.bytes **************** * Utility object for formatting byte values */ Morebits.bytes = function( value ) { if( typeof value === 'string' ) { var res = /(\d+) ?(\w?)(i?)B?/.exec( value ); var number = res[1]; var mag = res[2]; var si = res[3]; if( !number ) { this.number = 0; return; } if( !si ) { this.value = number * Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 ); } else { this.value = number * Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 ); } } else { this.value = value; } }; Morebits.bytes.magnitudes = { '': 0, 'K': 1, 'M': 2, 'G': 3, 'T': 4, 'P': 5, 'E': 6, 'Z': 7, 'Y': 8 }; Morebits.bytes.rmagnitudes = { 0: '', 1: 'K', 2: 'M', 3: 'G', 4: 'T', 5: 'P', 6: 'E', 7: 'Z', 8: 'Y' }; Morebits.bytes.prototype.valueOf = function() { return this.value; }; Morebits.bytes.prototype.toString = function( magnitude ) { var tmp = this.value; if( magnitude ) { var si = /i/.test(magnitude); var mag = magnitude.replace( /.*?(\w)i?B?.*/g, '$1' ); if( si ) { tmp /= Math.pow( 2, Morebits.bytes.magnitudes[mag] * 10 ); } else { tmp /= Math.pow( 10, Morebits.bytes.magnitudes[mag] * 3 ); } if( parseInt( tmp, 10 ) !== tmp ) { tmp = Number( tmp ).toPrecision( 4 ); } return tmp + ' ' + mag + (si?'i':'') + 'B'; } else { // si per default var current = 0; while( tmp >= 1024 ) { tmp /= 1024; ++current; } tmp = this.value / Math.pow( 2, current * 10 ); if( parseInt( tmp, 10 ) !== tmp ) { tmp = Number( tmp ).toPrecision( 4 ); } return tmp + ' ' + Morebits.bytes.rmagnitudes[current] + ( current > 0 ? 'iB' : 'B' ); } }; /** * **************** String; Morebits.string **************** */ if (!String.prototype.trimLeft) { String.prototype.trimLeft = function stringPrototypeLtrim( chars ) { chars = chars || "\\s"; return this.replace( new RegExp("^[" + chars + "]+", "g"), "" ); }; } if (!String.prototype.trimRight) { String.prototype.trimRight = function stringPrototypeRtrim( chars ) { chars = chars || "\\s"; return this.replace( new RegExp("[" + chars + "]+$", "g"), "" ); }; } if (!String.prototype.trim) { String.prototype.trim = function stringPrototypeTrim( chars ) { return this.trimRight(chars).trimLeft(chars); }; } // Helper functions to change case of a string Morebits.string = { toUpperCaseFirstChar: function(str) { str = str.toString(); return str.substr( 0, 1 ).toUpperCase() + str.substr( 1 ); }, toLowerCaseFirstChar: function(str) { str = str.toString(); return str.substr( 0, 1 ).toLowerCase() + str.substr( 1 ); }, splitWeightedByKeys: function( str, start, end, skip ) { if( start.length !== end.length ) { throw new Error( 'start marker and end marker must be of the same length' ); } var level = 0; var initial = null; var result = []; if( ! $.isArray( skip ) ) { if( skip === undefined ) { skip = []; } else if( typeof skip === 'string' ) { skip = [ skip ]; } else { throw new Error( "non-applicable skip parameter" ); } } for( var i = 0; i < str.length; ++i ) { for( var j = 0; j < skip.length; ++j ) { if( str.substr( i, skip[j].length ) === skip[j] ) { i += skip[j].length - 1; continue; } } if( str.substr( i, start.length ) === start ) { if( initial === null ) { initial = i; } ++level; i += start.length - 1; } else if( str.substr( i, end.length ) === end ) { --level; i += end.length - 1; } if( !level && initial !== null ) { result.push( str.substring( initial, i + 1 ) ); initial = null; } } return result; } }; /** * **************** Morebits.array **************** * * uniq(arr): returns a copy of the array with duplicates removed * * dups(arr): returns a copy of the array with the first instance of each value * removed; subsequent instances of those values (duplicates) remain * * chunk(arr, size): breaks up |arr| into smaller arrays of length |size|, and * returns an array of these "chunked" arrays */ Morebits.array = { uniq: function(arr) { if ( ! $.isArray( arr ) ) { throw "A non-array object passed to Morebits.array.uniq"; } var result = []; for( var i = 0; i < arr.length; ++i ) { var current = arr[i]; if( result.indexOf( current ) === -1 ) { result.push( current ); } } return result; }, dups: function(arr) { if ( ! $.isArray( arr ) ) { throw "A non-array object passed to Morebits.array.dups"; } var uniques = []; var result = []; for( var i = 0; i < arr.length; ++i ) { var current = arr[i]; if( uniques.indexOf( current ) === -1 ) { uniques.push( current ); } else { result.push( current ); } } return result; }, chunk: function( arr, size ) { if ( ! $.isArray( arr ) ) { throw "A non-array object passed to Morebits.array.chunk"; } if( typeof size !== 'number' || size <= 0 ) { // pretty impossible to do anything :) return [ arr ]; // we return an array consisting of this array. } var result = []; var current; for( var i = 0; i < arr.length; ++i ) { if( i % size === 0 ) { // when 'i' is 0, this is always true, so we start by creating one. current = []; result.push( current ); } current.push( arr[i] ); } return result; } }; /** * **************** Morebits.getPageAssociatedUser **************** * Get the user associated with the currently-viewed page. * Currently works on User:, User talk:, Special:Contributions. */ Morebits.getPageAssociatedUser = function(){ var thisNamespaceId = mw.config.get('wgNamespaceNumber'); if ( thisNamespaceId === 2 /* User: */ || thisNamespaceId === 3 /* User talk: */ ) { return mw.config.get('wgTitle').split( '/' )[0]; // only first part before any slashes, to work on subpages } if ( thisNamespaceId === -1 /* Special: */ && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) { return mw.config.get("wgRelevantUserName"); } return false; }; /** * **************** Morebits.unbinder **************** * Used by Morebits.wikitext.page.commentOutImage */ Morebits.unbinder = function Unbinder( string ) { if( typeof string !== 'string' ) { throw new Error( "not a string" ); } this.content = string; this.counter = 0; this.history = {}; this.prefix = '%UNIQ::' + Math.random() + '::'; this.postfix = '::UNIQ%'; } Morebits.unbinder.prototype = { unbind: function UnbinderUnbind( prefix, postfix ) { var re = new RegExp( prefix + '(.*?)' + postfix, 'g' ); this.content = this.content.replace( re, Morebits.unbinder.getCallback( this ) ); }, rebind: function UnbinderRebind() { var content = this.content; content.self = this; for( var current in this.history ) { if( this.history.hasOwnProperty( current ) ) { content = content.replace( current, this.history[current] ); } } return content; }, prefix: null, // %UNIQ::0.5955981644938324:: postfix: null, // ::UNIQ% content: null, // string counter: null, // 0++ history: null // {} }; Morebits.unbinder.getCallback = function UnbinderGetCallback(self) { return function UnbinderCallback( match , a , b ) { var current = self.prefix + self.counter + self.postfix; self.history[current] = match; ++self.counter; return current; }; }; /** * **************** Date **************** * Helper functions to get the month as a string instead of a number * * Normally it is poor form to play with prototypes of primitive types, but it * is fairly unlikely that anyone will iterate over a Date object. */ Date.monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; Date.monthNamesAbbrev = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; Date.prototype.getMonthName = function() { return Date.monthNames[ this.getMonth() ]; }; Date.prototype.getMonthNameAbbrev = function() { return Date.monthNamesAbbrev[ this.getMonth() ]; }; Date.prototype.getUTCMonthName = function() { return Date.monthNames[ this.getUTCMonth() ]; }; Date.prototype.getUTCMonthNameAbbrev = function() { return Date.monthNamesAbbrev[ this.getUTCMonth() ]; }; /** * **************** Morebits.wikipedia **************** * English Wikipedia-specific objects */ Morebits.wikipedia = {}; Morebits.wikipedia.namespaces = { '-2': 'Media', '-1': 'Special', '0': '', '1': 'Talk', '2': 'User', '3': 'User talk', '4': 'Project', '5': 'Project talk', '6': 'File', '7': 'File talk', '8': 'MediaWiki', '9': 'MediaWiki talk', '10': 'Template', '11': 'Template talk', '12': 'Help', '13': 'Help talk', '14': 'Category', '15': 'Category talk', '100': 'Portal', '101': 'Portal talk', '108': 'Book', '109': 'Book talk' }; Morebits.wikipedia.namespacesFriendly = { '0': '(Article)', '1': 'Talk', '2': 'User', '3': 'User talk', '4': 'Wikipedia', '5': 'Wikipedia talk', '6': 'File', '7': 'File talk', '8': 'MediaWiki', '9': 'MediaWiki talk', '10': 'Template', '11': 'Template talk', '12': 'Help', '13': 'Help talk', '14': 'Category', '15': 'Category talk', '100': 'Portal', '101': 'Portal talk', '108': 'Book', '109': 'Book talk' }; /** * **************** Morebits.wiki **************** * Various objects for wiki editing and API access */ Morebits.wiki = {}; // Analyzes the HTML of the current page (i.e. no AJAX requests) to determine if it // is a redirect or soft redirect Morebits.wiki.isPageRedirect = function wikipediaIsPageRedirect() { return !!($("span.redirectText").length > 0 || document.getElementById("softredirect")); }; /** * **************** Morebits.wiki.actionCompleted **************** * * Use of Morebits.wiki.actionCompleted(): * Every call to Morebits.wiki.api.post() results in the dispatch of * an asynchronous callback. Each callback can in turn * make an additional call to Morebits.wiki.api.post() to continue a * processing sequence. At the conclusion of the final callback * of a processing sequence, it is not possible to simply return to the * original caller because there is no call stack leading back to * the original context. Instead, Morebits.wiki.actionCompleted.event() is * called to display the result to the user and to perform an optional * page redirect. * * The determination of when to call Morebits.wiki.actionCompleted.event() * is managed through the globals Morebits.wiki.numberOfActionsLeft and * Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is * incremented at the start of every Morebits.wiki.api call and decremented * after the completion of a callback function. If a callback function * does not create a new Morebits.wiki.api object before exiting, it is the * final step in the processing chain and Morebits.wiki.actionCompleted.event() * will then be called. * * Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that * processing is not complete upon the conclusion of the final callback function. * This is used for batch operations. The end of a batch is signaled by calling * Morebits.wiki.removeCheckpoint(). */ Morebits.wiki.numberOfActionsLeft = 0; Morebits.wiki.nbrOfCheckpointsLeft = 0; Morebits.wiki.actionCompleted = function( self ) { if( --Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0 ) { Morebits.wiki.actionCompleted.event( self ); } }; // Change per action wanted Morebits.wiki.actionCompleted.event = function() { new Morebits.status( Morebits.wiki.actionCompleted.notice, Morebits.wiki.actionCompleted.postfix, 'info' ); if( Morebits.wiki.actionCompleted.redirect ) { // if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones. if( !( (/^\w+\:\/\//).test( Morebits.wiki.actionCompleted.redirect ) ) ) { Morebits.wiki.actionCompleted.redirect = mw.util.getUrl( Morebits.wiki.actionCompleted.redirect ); if( Morebits.wiki.actionCompleted.followRedirect === false ) { Morebits.wiki.actionCompleted.redirect += "?redirect=no"; } } window.setTimeout( function() { window.location = Morebits.wiki.actionCompleted.redirect; }, Morebits.wiki.actionCompleted.timeOut ); } }; Morebits.wiki.actionCompleted.timeOut = ( typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut ); Morebits.wiki.actionCompleted.redirect = null; Morebits.wiki.actionCompleted.notice = 'Action'; Morebits.wiki.actionCompleted.postfix = 'completed'; Morebits.wiki.addCheckpoint = function() { ++Morebits.wiki.nbrOfCheckpointsLeft; }; Morebits.wiki.removeCheckpoint = function() { if( --Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0 ) { Morebits.wiki.actionCompleted.event(); } }; /** * **************** Morebits.wiki.api **************** * An easy way to talk to the MediaWiki API. * * Constructor parameters: * currentAction: the current action (required) * query: the query (required) * onSuccess: the function to call when request gotten * statusElement: a Morebits.status object to use for status messages (optional) * onError: the function to call if an error occurs (optional) */ Morebits.wiki.api = function( currentAction, query, onSuccess, statusElement, onError ) { this.currentAction = currentAction; this.query = query; this.query.format = 'xml'; this.query.assert = 'user'; this.onSuccess = onSuccess; this.onError = onError; if( statusElement ) { this.statelem = statusElement; this.statelem.status( currentAction ); } else { this.statelem = new Morebits.status( currentAction ); } }; Morebits.wiki.api.prototype = { currentAction: '', onSuccess: null, onError: null, parent: window, // use global context if there is no parent object query: null, responseXML: null, setParent: function(parent) { this.parent = parent; }, // keep track of parent object for callbacks statelem: null, // this non-standard name kept for backwards compatibility statusText: null, // result received from the API, normally "success" or "error" errorCode: null, // short text error code, if any, as documented in the MediaWiki API errorText: null, // full error description, if any // post(): carries out the request // do not specify a parameter unless you really really want to give jQuery some extra parameters post: function( callerAjaxParameters ) { ++Morebits.wiki.numberOfActionsLeft; var ajaxparams = $.extend( {}, { context: this, type: 'POST', url: mw.util.wikiScript('api'), data: Morebits.queryString.create(this.query), datatype: 'xml', headers: { 'Api-User-Agent': morebitsWikiApiUserAgent }, success: function(xml, statusText, jqXHR) { this.statusText = statusText; this.responseXML = xml; this.errorCode = $(xml).find('error').attr('code'); this.errorText = $(xml).find('error').attr('info'); if (typeof this.errorCode === "string") { // the API didn't like what we told it, e.g., bad edit token or an error creating a page this.returnError(); return; } // invoke success callback if one was supplied if (this.onSuccess) { // set the callback context to this.parent for new code and supply the API object // as the first argument to the callback (for legacy code) this.onSuccess.call( this.parent, this ); } else { this.statelem.info("done"); } Morebits.wiki.actionCompleted(); }, // only network and server errors reach here – complaints from the API itself are caught in success() error: function(jqXHR, statusText, errorThrown) { this.statusText = statusText; this.errorThrown = errorThrown; // frequently undefined this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.'; this.returnError(); } }, callerAjaxParameters ); return $.ajax( ajaxparams ); // the return value should be ignored, unless using callerAjaxParameters with |async: false| }, returnError: function() { this.statelem.error( this.errorText ); // invoke failure callback if one was supplied if (this.onError) { // set the callback context to this.parent for new code and supply the API object // as the first argument to the callback for legacy code this.onError.call( this.parent, this ); } // don't complete the action so that the error remains displayed }, getStatusElement: function() { return this.statelem; }, getErrorCode: function() { return this.errorCode; }, getErrorText: function() { return this.errorText; }, getXML: function() { return this.responseXML; } }; // Custom user agent header, used by WMF for server-side logging // See https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html var morebitsWikiApiUserAgent = 'morebits.js/2.0 ([[w:WT:TW]])'; // Sets the custom user agent header Morebits.wiki.api.setApiUserAgent = function( ua ) { morebitsWikiApiUserAgent = ( ua ? ua + ' ' : '' ) + 'morebits.js/2.0 ([[w:WT:TW]])'; }; /** * **************** Morebits.wiki.page **************** * Uses the MediaWiki API to load a page and optionally edit it, move it, etc. * * Callers are not permitted to directly access the properties of this class! * All property access is through the appropriate get___() or set___() method. * * Callers should set Morebits.wiki.actionCompleted.notice and Morebits.wiki.actionCompleted.redirect * before the first call to Morebits.wiki.page.load(). * * Each of the callback functions takes one parameter, which is a * reference to the Morebits.wiki.page object that registered the callback. * Callback functions may invoke any Morebits.wiki.page prototype method using this reference. * * * NOTE: This list of member functions is incomplete. * * Constructor: Morebits.wiki.page(pageName, currentAction) * pageName - the name of the page, prefixed by the namespace (if any) * (for the current page, use mw.config.get('wgPageName')) * currentAction - a string describing the action about to be undertaken (optional) * * load(onSuccess, onFailure): Loads the text for the page * onSuccess - callback function which is called when the load has succeeded * onFailure - callback function which is called when the load fails (optional) * XXX onFailure for load() is not yet implemented – do we need it? -- UncleDouggie * probably not -- TTO * * save(onSuccess, onFailure): Saves the text for the page. Must be preceded by calling load(). * onSuccess - callback function which is called when the save has succeeded (optional) * onFailure - callback function which is called when the save fails (optional) * Warning: Calling save() can result in additional calls to the previous load() callbacks to * recover from edit conflicts! * In this case, callers must make the same edit to the new pageText and reinvoke save(). * This behavior can be disabled with setMaxConflictRetries(0). * * append(onSuccess, onFailure): Adds the text provided via setAppendText() to the end of the page. * Does not require calling load() first. * onSuccess - callback function which is called when the method has succeeded (optional) * onFailure - callback function which is called when the method fails (optional) * * prepend(onSuccess, onFailure): Adds the text provided via setPrependText() to the start of the page. * Does not require calling load() first. * onSuccess - callback function which is called when the method has succeeded (optional) * onFailure - callback function which is called when the method fails (optional) * * getPageName(): returns a string containing the name of the loaded page, including the namespace * * getPageText(): returns a string containing the text of the page after a successful load() * * setPageText(pageText) * pageText - string containing the updated page text that will be saved when save() is called * * setAppendText(appendText) * appendText - string containing the text that will be appended to the page when append() is called * * setPrependText(prependText) * prependText - string containing the text that will be prepended to the page when prepend() is called * * setEditSummary(summary) * summary - string containing the text of the edit summary that will be used when save() is called * * setMinorEdit(minorEdit) * minorEdit is a boolean value: * true - When save is called, the resulting edit will be marked as "minor". * false - When save is called, the resulting edit will not be marked as "minor". (default) * * setPageSection(pageSection) * pageSection - integer specifying the section number to load or save. The default is |null|, which means * that the entire page will be retrieved. * * setMaxConflictRetries(maxRetries) * maxRetries - number of retries for save errors involving an edit conflict or loss of edit token * default: 2 * * setMaxRetries(maxRetries) * maxRetries - number of retries for save errors not involving an edit conflict or loss of edit token * default: 2 * * setCallbackParameters(callbackParameters) * callbackParameters - an object for use in a callback function * * getCallbackParameters(): returns the object previous set by setCallbackParameters() * * Callback notes: callbackParameters is for use by the caller only. The parameters * allow a caller to pass the proper context into its callback function. * Callers must ensure that any changes to the callbackParameters object * within a load() callback still permit a proper re-entry into the * load() callback if an edit conflict is detected upon calling save(). * * getStatusElement(): returns the Status element created by the constructor * * setFollowRedirect(followRedirect) * followRedirect is a boolean value: * true - a maximum of one redirect will be followed. * In the event of a redirect, a message is displayed to the user and * the redirect target can be retrieved with getPageName(). * false - the requested pageName will be used without regard to any redirect. (default) * * setWatchlist(watchlistOption) * watchlistOption is a boolean value: * true - page will be added to the user's watchlist when save() is called * false - watchlist status of the page will not be changed (default) * * setWatchlistFromPreferences(watchlistOption) * watchlistOption is a boolean value: * true - page watchlist status will be set based on the user's * preference settings when save() is called * false - watchlist status of the page will not be changed (default) * * Watchlist notes: * 1. The MediaWiki API value of 'unwatch', which explicitly removes the page from the * user's watchlist, is not used. * 2. If both setWatchlist() and setWatchlistFromPreferences() are called, * the last call takes priority. * 3. Twinkle modules should use the appropriate preference to set the watchlist options. * 4. Most Twinkle modules use setWatchlist(). * setWatchlistFromPreferences() is only needed for the few Twinkle watchlist preferences * that accept a string value of 'default'. * * setCreateOption(createOption) * createOption is a string value: * 'recreate' - create the page if it does not exist, or edit it if it exists * 'createonly' - create the page if it does not exist, but return an error if it * already exists * 'nocreate' - don't create the page, only edit it if it already exists * null - create the page if it does not exist, unless it was deleted in the moment * between retrieving the edit token and saving the edit (default) * * exists(): returns true if the page existed on the wiki when it was last loaded * * lookupCreator(onSuccess): Retrieves the username of the user who created the page * onSuccess - callback function which is called when the username is found * within the callback, the username can be retrieved using the getCreator() function * * getCreator(): returns the user who created the page following lookupCreator() * * patrol(): marks the page as patrolled (only when "rcid" is present in the query string) * * move(onSuccess, onFailure): Moves a page to another title * * deletePage(onSuccess, onFailure): Deletes a page (for admins only) * */ /** * Call sequence for common operations (optional final user callbacks not shown): * * Edit current contents of a page (no edit conflict): * .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() * * Edit current contents of a page (with edit conflict): * .load(userTextEditCallback) -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveError() -> * ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() * * Append to a page (similar for prepend): * .append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() -> * ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> * ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess() * * Notes: * 1. All functions following Morebits.wiki.api.post() are invoked asynchronously * from the jQuery AJAX library. * 2. The sequence for append/prepend could be slightly shortened, but it would require * significant duplication of code for little benefit. */ Morebits.wiki.page = function(pageName, currentAction) { if (!currentAction) { currentAction = 'Opening page "' + pageName + '"'; } /** * Private context variables * * This context is not visible to the outside, thus all the data here * must be accessed via getter and setter functions. */ var ctx = { // backing fields for public properties pageName: pageName, pageExists: false, editSummary: null, callbackParameters: null, statusElement: new Morebits.status(currentAction), // - edit pageText: null, editMode: 'all', // save() replaces entire contents of the page by default appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect createOption: null, minorEdit: false, pageSection: null, maxConflictRetries: 2, maxRetries: 2, followRedirect: false, watchlistOption: 'nochange', creator: null, // - revert revertOldID: null, // - move moveDestination: null, moveTalkPage: false, moveSubpages: false, moveSuppressRedirect: false, // - protect protectEdit: null, protectMove: null, protectCreate: null, protectCascade: false, // - stabilize (FlaggedRevs) flaggedRevs: null, // internal status pageLoaded: false, editToken: null, loadTime: null, lastEditTime: null, revertCurID: null, revertUser: null, fullyProtected: false, conflictRetries: 0, retries: 0, // callbacks onLoadSuccess: null, onLoadFailure: null, onSaveSuccess: null, onSaveFailure: null, onLookupCreatorSuccess: null, onMoveSuccess: null, onMoveFailure: null, onDeleteSuccess: null, onDeleteFailure: null, onProtectSuccess: null, onProtectFailure: null, onStabilizeSuccess: null, onStabilizeFailure: null, // internal objects loadQuery: null, loadApi: null, saveApi: null, lookupCreatorApi: null, moveApi: null, moveProcessApi: null, deleteApi: null, deleteProcessApi: null, protectApi: null, protectProcessApi: null, stabilizeApi: null, stabilizeProcessApi: null }; var emptyFunction = function() { }; /** * Public interface accessors */ this.getPageName = function() { return ctx.pageName; }; this.getPageText = function() { return ctx.pageText; }; this.setPageText = function(pageText) { ctx.editMode = 'all'; ctx.pageText = pageText; }; this.setAppendText = function(appendText) { ctx.editMode = 'append'; ctx.appendText = appendText; }; this.setPrependText = function(prependText) { ctx.editMode = 'prepend'; ctx.prependText = prependText; }; this.setEditSummary = function(summary) { ctx.editSummary = summary; }; this.setCreateOption = function(createOption) { ctx.createOption = createOption; }; this.setMinorEdit = function(minorEdit) { ctx.minorEdit = minorEdit; }; this.setPageSection = function(pageSection) { ctx.pageSection = pageSection; }; this.setMaxConflictRetries = function(maxRetries) { ctx.maxConflictRetries = maxRetries; }; this.setMaxRetries = function(maxRetries) { ctx.maxRetries = maxRetries; }; this.setCallbackParameters = function(callbackParameters) { ctx.callbackParameters = callbackParameters; }; this.getCallbackParameters = function() { return ctx.callbackParameters; }; this.getCreator = function() { return ctx.creator; }; this.setOldID = function(oldID) { ctx.revertOldID = oldID; }; this.getRevisionUser = function() { return ctx.revertUser; }; this.setMoveDestination = function(destination) { ctx.moveDestination = destination; }; this.setMoveTalkPage = function(flag) { ctx.moveTalkPage = !!flag; }; this.setMoveSubpages = function(flag) { ctx.moveSubpages = !!flag; }; this.setMoveSuppressRedirect = function(flag) { ctx.moveSuppressRedirect = !!flag; }; this.setEditProtection = function(level, expiry) { ctx.protectEdit = { level: level, expiry: expiry }; }; this.setMoveProtection = function(level, expiry) { ctx.protectMove = { level: level, expiry: expiry }; }; this.setCreateProtection = function(level, expiry) { ctx.protectCreate = { level: level, expiry: expiry }; }; this.setCascadingProtection = function(flag) { ctx.protectCascade = !!flag; }; this.setFlaggedRevs = function(level, expiry) { ctx.flaggedRevs = { level: level, expiry: expiry }; }; this.getStatusElement = function() { return ctx.statusElement; }; this.setFollowRedirect = function(followRedirect) { if (ctx.pageLoaded) { ctx.statusElement.error("Internal error: cannot change redirect setting after the page has been loaded!"); return; } ctx.followRedirect = followRedirect; }; this.setWatchlist = function(flag) { if (flag) { ctx.watchlistOption = 'watch'; } else { ctx.watchlistOption = 'nochange'; } }; this.setWatchlistFromPreferences = function(flag) { if (flag) { ctx.watchlistOption = 'preferences'; } else { ctx.watchlistOption = 'nochange'; } }; this.exists = function() { return ctx.pageExists; }; this.load = function(onSuccess, onFailure) { ctx.onLoadSuccess = onSuccess; ctx.onLoadFailure = onFailure || emptyFunction; // Need to be able to do something after the page loads if (!onSuccess) { ctx.statusElement.error("Internal error: no onSuccess callback provided to load()!"); ctx.onLoadFailure(this); return; } ctx.loadQuery = { action: 'query', prop: 'info|revisions', curtimestamp: '', meta: 'tokens', type: 'csrf', titles: ctx.pageName // don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default }; if (ctx.editMode === 'all') { ctx.loadQuery.rvprop = 'content'; // get the page content at the same time, if needed } else if (ctx.editMode === 'revert') { ctx.loadQuery.rvlimit = 1; ctx.loadQuery.rvstartid = ctx.revertOldID; } if (ctx.followRedirect) { ctx.loadQuery.redirects = ''; // follow all redirects } if (typeof ctx.pageSection === 'number') { ctx.loadQuery.rvsection = ctx.pageSection; } if (Morebits.userIsInGroup('sysop')) { ctx.loadQuery.inprop = 'protection'; } ctx.loadApi = new Morebits.wiki.api("Retrieving page...", ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure); ctx.loadApi.setParent(this); ctx.loadApi.post(); }; // Save updated .pageText to Wikipedia // Only valid after successful .load() this.save = function(onSuccess, onFailure) { ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; if (!ctx.pageLoaded) { ctx.statusElement.error("Internal error: attempt to save a page that has not been loaded!"); ctx.onSaveFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: edit summary not set before save!"); ctx.onSaveFailure(this); return; } if (ctx.fullyProtected && !confirm('You are about to make an edit to the fully protected page "' + ctx.pageName + (ctx.fullyProtected === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + ctx.fullyProtected + ')')) + '. \n\nClick OK to proceed with the edit, or Cancel to skip this edit.')) { ctx.statusElement.error("Edit to fully protected page was aborted."); ctx.onSaveFailure(this); return; } ctx.retries = 0; var query = { action: 'edit', title: ctx.pageName, summary: ctx.editSummary, token: ctx.editToken, watchlist: ctx.watchlistOption }; if (typeof ctx.pageSection === 'number') { query.section = ctx.pageSection; } // Set minor edit attribute. If these parameters are present with any value, it is interpreted as true if (ctx.minorEdit) { query.minor = true; } else { query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor" } switch (ctx.editMode) { case 'append': query.appendtext = ctx.appendText; // use mode to append to current page contents break; case 'prepend': query.prependtext = ctx.prependText; // use mode to prepend to current page contents break; case 'revert': query.undo = ctx.revertCurID; query.undoafter = ctx.revertOldID; if (ctx.lastEditTime) { query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded } query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff) break; default: query.text = ctx.pageText; // replace entire contents of the page if (ctx.lastEditTime) { query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded } query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff) break; } if (['recreate', 'createonly', 'nocreate'].indexOf(ctx.createOption) !== -1) { query[ctx.createOption] = ''; } ctx.saveApi = new Morebits.wiki.api( "Saving page...", query, fnSaveSuccess, ctx.statusElement, fnSaveError); ctx.saveApi.setParent(this); ctx.saveApi.post(); }; this.append = function(onSuccess, onFailure) { ctx.editMode = 'append'; ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; this.load(fnAutoSave, ctx.onSaveFailure); }; this.prepend = function(onSuccess, onFailure) { ctx.editMode = 'prepend'; ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; this.load(fnAutoSave, ctx.onSaveFailure); }; this.lookupCreator = function(onSuccess) { if (!onSuccess) { ctx.statusElement.error("Internal error: no onSuccess callback provided to lookupCreator()!"); return; } ctx.onLookupCreatorSuccess = onSuccess; var query = { 'action': 'query', 'prop': 'revisions', 'titles': ctx.pageName, 'rvlimit': 1, 'rvprop': 'user', 'rvdir': 'newer' }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.lookupCreatorApi = new Morebits.wiki.api("Retrieving page creator information", query, fnLookupCreatorSuccess, ctx.statusElement); ctx.lookupCreatorApi.setParent(this); ctx.lookupCreatorApi.post(); }; this.patrol = function() { // look for rcid in querystring; if not, we won't have a patrol token, so no point trying if (!Morebits.queryString.exists("rcid")) { return; } var rcid = Morebits.queryString.get("rcid"); // extract patrol token from "Mark page as patrolled" link on page var patrollinkmatch = /token=(.+)%2B%5C$/.exec($(".patrollink a").attr("href")); if (patrollinkmatch) { var patroltoken = patrollinkmatch[1] + "+\\"; var patrolstat = new Morebits.status("Marking page as patrolled"); var wikipedia_api = new Morebits.wiki.api("doing...", { title: ctx.pageName, action: 'markpatrolled', rcid: rcid, token: patroltoken }, null, patrolstat); wikipedia_api.post({ type: 'GET', url: mw.util.wikiScript('index'), datatype: 'text' // we don't really care about the response }); } }; this.revert = function(onSuccess, onFailure) { ctx.onSaveSuccess = onSuccess; ctx.onSaveFailure = onFailure || emptyFunction; if (!ctx.revertOldID) { ctx.statusElement.error("Internal error: revision ID to revert to was not set before revert!"); ctx.onSaveFailure(this); return; } ctx.editMode = 'revert'; this.load(fnAutoSave, ctx.onSaveFailure); }; this.move = function(onSuccess, onFailure) { ctx.onMoveSuccess = onSuccess; ctx.onMoveFailure = onFailure || emptyFunction; if (!ctx.editSummary) { ctx.statusElement.error("Internal error: move reason not set before move (use setEditSummary function)!"); ctx.onMoveFailure(this); return; } if (!ctx.moveDestination) { ctx.statusElement.error("Internal error: destination page name was not set before move!"); ctx.onMoveFailure(this); return; } var query = { action: 'query', prop: 'info', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } if (Morebits.userIsInGroup('sysop')) { query.inprop = 'protection'; } ctx.moveApi = new Morebits.wiki.api("retrieving move token...", query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure); ctx.moveApi.setParent(this); ctx.moveApi.post(); }; // |delete| is a reserved word in some flavours of JS this.deletePage = function(onSuccess, onFailure) { ctx.onDeleteSuccess = onSuccess; ctx.onDeleteFailure = onFailure || emptyFunction; // if a non-admin tries to do this, don't bother if (!Morebits.userIsInGroup('sysop')) { ctx.statusElement.error("Cannot delete page: only admins can do that"); ctx.onDeleteFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: delete reason not set before delete (use setEditSummary function)!"); ctx.onDeleteFailure(this); return; } var query = { action: 'query', prop: 'info', inprop: 'protection', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.deleteApi = new Morebits.wiki.api("retrieving delete token...", query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure); ctx.deleteApi.setParent(this); ctx.deleteApi.post(); }; this.protect = function(onSuccess, onFailure) { ctx.onProtectSuccess = onSuccess; ctx.onProtectFailure = onFailure || emptyFunction; // if a non-admin tries to do this, don't bother if (!Morebits.userIsInGroup('sysop')) { ctx.statusElement.error("Cannot protect page: only admins can do that"); ctx.onProtectFailure(this); return; } if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) { ctx.statusElement.error("Internal error: you must set edit and/or move and/or create protection before calling protect()!"); ctx.onProtectFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: protection reason not set before protect (use setEditSummary function)!"); ctx.onProtectFailure(this); return; } var query = { action: 'query', prop: 'info', inprop: 'protection', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.protectApi = new Morebits.wiki.api("retrieving protect token...", query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure); ctx.protectApi.setParent(this); ctx.protectApi.post(); }; // apply FlaggedRevs protection-style settings // only works where $wgFlaggedRevsProtection = true (i.e. where FlaggedRevs // settings appear on the wiki's "protect" tab) this.stabilize = function(onSuccess, onFailure) { ctx.onStabilizeSuccess = onSuccess; ctx.onStabilizeFailure = onFailure || emptyFunction; // if a non-admin tries to do this, don't bother if (!Morebits.userIsInGroup('sysop')) { ctx.statusElement.error("Cannot apply FlaggedRevs settings: only admins can do that"); ctx.onStabilizeFailure(this); return; } if (!ctx.flaggedRevs) { ctx.statusElement.error("Internal error: you must set flaggedRevs before calling stabilize()!"); ctx.onStabilizeFailure(this); return; } if (!ctx.editSummary) { ctx.statusElement.error("Internal error: reason not set before calling stabilize() (use setEditSummary function)!"); ctx.onStabilizeFailure(this); return; } var query = { action: 'query', prop: 'info|flagged', meta: 'tokens', type: 'csrf', titles: ctx.pageName }; if (ctx.followRedirect) { query.redirects = ''; // follow all redirects } ctx.stabilizeApi = new Morebits.wiki.api("retrieving stabilize token...", query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure); ctx.stabilizeApi.setParent(this); ctx.stabilizeApi.post(); }; /** * Private member functions * * These are not exposed outside */ // callback from loadSuccess() for append() and prepend() threads var fnAutoSave = function(pageobj) { pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure); }; // callback from loadApi.post() var fnLoadSuccess = function() { var xml = ctx.loadApi.getXML(); if ( !fnCheckPageName(xml, ctx.onLoadFailure) ) { return; // abort } ctx.pageExists = ($(xml).find('page').attr('missing') !== ""); if (ctx.pageExists) { ctx.pageText = $(xml).find('rev').text(); } else { ctx.pageText = ''; // allow for concatenation, etc. } // extract protection info, to alert admins when they are about to edit a protected page if (Morebits.userIsInGroup('sysop')) { var editprot = $(xml).find('pr[type="edit"]'); if (editprot.length > 0 && editprot.attr('level') === 'sysop') { ctx.fullyProtected = editprot.attr('expiry'); } else { ctx.fullyProtected = false; } } ctx.editToken = $(xml).find('tokens').attr('csrftoken'); if (!ctx.editToken) { ctx.statusElement.error("Failed to retrieve edit token."); ctx.onLoadFailure(this); return; } ctx.loadTime = $(xml).find('api').attr('curtimestamp'); if (!ctx.loadTime) { ctx.statusElement.error("Failed to retrieve start timestamp."); ctx.onLoadFailure(this); return; } ctx.lastEditTime = $(xml).find('page').attr('touched'); if (ctx.editMode === 'revert') { ctx.revertCurID = $(xml).find('rev').attr('revid'); if (!ctx.revertCurID) { ctx.statusElement.error("Failed to retrieve current revision ID."); ctx.onLoadFailure(this); return; } ctx.revertUser = $(xml).find('rev').attr('user'); if (!ctx.revertUser) { if ($(xml).find('rev').attr('userhidden') === "") { // username was RevDel'd or oversighted ctx.revertUser = "<username hidden>"; } else { ctx.statusElement.error("Failed to retrieve user who made the revision."); ctx.onLoadFailure(this); return; } } // set revert edit summary ctx.editSummary = "[[Help:Revert|Reverted]] to revision " + ctx.revertOldID + " by " + ctx.revertUser + ": " + ctx.editSummary; } ctx.pageLoaded = true; // alert("Generate edit conflict now"); // for testing edit conflict recovery logic ctx.onLoadSuccess(this); // invoke callback }; // helper function to parse the page name returned from the API var fnCheckPageName = function(xml, onFailure) { if (!onFailure) { onFailure = emptyFunction; } // check for invalid titles if ( $(xml).find('page').attr('invalid') === "" ) { ctx.statusElement.error("The page title is invalid: " + ctx.pageName); onFailure(this); return false; // abort } // retrieve actual title of the page after normalization and redirects if ( $(xml).find('page').attr('title') ) { var resolvedName = $(xml).find('page').attr('title'); // only notify user for redirects, not normalization if ( $(xml).find('redirects').length > 0 ) { Morebits.status.info("Info", "Redirected from " + ctx.pageName + " to " + resolvedName ); } ctx.pageName = resolvedName; // always update in case of normalization } else { // could be a circular redirect or other problem ctx.statusElement.error("Could not resolve redirects for: " + ctx.pageName); onFailure(this); // force error to stay on the screen ++Morebits.wiki.numberOfActionsLeft; return false; // abort } return true; // all OK }; // callback from saveApi.post() var fnSaveSuccess = function() { ctx.editMode = 'all'; // cancel append/prepend/revert modes var xml = ctx.saveApi.getXML(); // see if the API thinks we were successful if ($(xml).find('edit').attr('result') === "Success") { // real success // default on success action - display link for edited page var link = document.createElement('a'); link.setAttribute('href', mw.util.getUrl(ctx.pageName) ); link.appendChild(document.createTextNode(ctx.pageName)); ctx.statusElement.info(['completed (', link, ')']); if (ctx.onSaveSuccess) { ctx.onSaveSuccess(this); // invoke callback } return; } // errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki // Wikimedia wikis should only return spam blacklist errors and captchas var blacklist = $(xml).find('edit').attr('spamblacklist'); if (blacklist) { var code = document.createElement('code'); code.style.fontFamily = "monospace"; code.appendChild(document.createTextNode(blacklist)); ctx.statusElement.error(['Could not save the page because the URL ', code, ' is on the spam blacklist.']); } else if ( $(xml).find('captcha').length > 0 ) { ctx.statusElement.error("Could not save the page because the wiki server wanted you to fill out a CAPTCHA."); } else { ctx.statusElement.error("Unknown error received from API while saving page"); } // force error to stay on the screen ++Morebits.wiki.numberOfActionsLeft; ctx.onSaveFailure(this); }; // callback from saveApi.post() var fnSaveError = function() { var errorCode = ctx.saveApi.getErrorCode(); // check for edit conflict if ( errorCode === "editconflict" && ctx.conflictRetries++ < ctx.maxConflictRetries ) { // edit conflicts can occur when the page needs to be purged from the server cache var purgeQuery = { action: 'purge', titles: ctx.pageName // redirects are already resolved }; var purgeApi = new Morebits.wiki.api("Edit conflict detected, purging server cache", purgeQuery, null, ctx.statusElement); var result = purgeApi.post( { async: false } ); // just wait for it, result is for debugging --Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds ctx.statusElement.info("Edit conflict detected, reapplying edit"); ctx.loadApi.post(); // reload the page and reapply the edit // check for loss of edit token // it's impractical to request a new token here, so invoke edit conflict logic when this happens } else if ( errorCode === "notoken" && ctx.conflictRetries++ < ctx.maxConflictRetries ) { ctx.statusElement.info("Edit token is invalid, retrying"); --Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds ctx.loadApi.post(); // reload // check for network or server error } else if ( errorCode === "undefined" && ctx.retries++ < ctx.maxRetries ) { // the error might be transient, so try again ctx.statusElement.info("Save failed, retrying"); --Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds ctx.saveApi.post(); // give it another go! // hard error, give up } else { // non-admin attempting to edit a protected page - this gives a friendlier message than the default if ( errorCode === "protectedpage" ) { ctx.statusElement.error( "Failed to save edit: Page is fully protected" ); } else { ctx.statusElement.error( "Failed to save edit: " + ctx.saveApi.getErrorText() ); } ctx.editMode = 'all'; // cancel append/prepend/revert modes if (ctx.onSaveFailure) { ctx.onSaveFailure(this); // invoke callback } } }; var fnLookupCreatorSuccess = function() { var xml = ctx.lookupCreatorApi.getXML(); if ( !fnCheckPageName(xml) ) { return; // abort } ctx.creator = $(xml).find('rev').attr('user'); if (!ctx.creator) { ctx.statusElement.error("Could not find name of page creator"); return; } ctx.onLookupCreatorSuccess(this); }; var fnProcessMove = function() { var xml = ctx.moveApi.getXML(); if ($(xml).find('page').attr('missing') === "") { ctx.statusElement.error("Cannot move the page, because it no longer exists"); ctx.onMoveFailure(this); return; } // extract protection info if (Morebits.userIsInGroup('sysop')) { var editprot = $(xml).find('pr[type="edit"]'); if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to move the fully protected page "' + ctx.pageName + (editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) + '. \n\nClick OK to proceed with the move, or Cancel to skip this move.')) { ctx.statusElement.error("Move of fully protected page was aborted."); ctx.onMoveFailure(this); return; } } var moveToken = $(xml).find('tokens').attr('csrftoken'); if (!moveToken) { ctx.statusElement.error("Failed to retrieve move token."); ctx.onMoveFailure(this); return; } var query = { 'action': 'move', 'from': $(xml).find('page').attr('title'), 'to': ctx.moveDestination, 'token': moveToken, 'reason': ctx.editSummary }; if (ctx.moveTalkPage) { query.movetalk = 'true'; } if (ctx.moveSubpages) { query.movesubpages = 'true'; // XXX don't know whether this works for non-admins } if (ctx.moveSuppressRedirect) { query.noredirect = 'true'; } if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.moveProcessApi = new Morebits.wiki.api("moving page...", query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure); ctx.moveProcessApi.setParent(this); ctx.moveProcessApi.post(); }; var fnProcessDelete = function() { var xml = ctx.deleteApi.getXML(); if ($(xml).find('page').attr('missing') === "") { ctx.statusElement.error("Cannot delete the page, because it no longer exists"); ctx.onDeleteFailure(this); return; } // extract protection info var editprot = $(xml).find('pr[type="edit"]'); if (editprot.length > 0 && editprot.attr('level') === 'sysop' && !confirm('You are about to delete the fully protected page "' + ctx.pageName + (editprot.attr('expiry') === 'infinity' ? '" (protected indefinitely)' : ('" (protection expiring ' + editprot.attr('expiry') + ')')) + '. \n\nClick OK to proceed with the deletion, or Cancel to skip this deletion.')) { ctx.statusElement.error("Deletion of fully protected page was aborted."); ctx.onDeleteFailure(this); return; } var deleteToken = $(xml).find('tokens').attr('csrftoken'); if (!deleteToken) { ctx.statusElement.error("Failed to retrieve delete token."); ctx.onDeleteFailure(this); return; } var query = { 'action': 'delete', 'title': $(xml).find('page').attr('title'), 'token': deleteToken, 'reason': ctx.editSummary }; if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.deleteProcessApi = new Morebits.wiki.api("deleting page...", query, ctx.onDeleteSuccess, ctx.statusElement, ctx.onDeleteFailure); ctx.deleteProcessApi.setParent(this); ctx.deleteProcessApi.post(); }; var fnProcessProtect = function() { var xml = ctx.protectApi.getXML(); var missing = ($(xml).find('page').attr('missing') === ""); if (((ctx.protectEdit || ctx.protectMove) && missing)) { ctx.statusElement.error("Cannot protect the page, because it no longer exists"); ctx.onProtectFailure(this); return; } if (ctx.protectCreate && !missing) { ctx.statusElement.error("Cannot create protect the page, because it already exists"); ctx.onProtectFailure(this); return; } // TODO cascading protection not possible on edit<sysop var protectToken = $(xml).find('tokens').attr('csrftoken'); if (!protectToken) { ctx.statusElement.error("Failed to retrieve protect token."); ctx.onProtectFailure(this); return; } // fetch existing protection levels var prs = $(xml).find('pr'); var editprot = prs.filter('[type="edit"]'); var moveprot = prs.filter('[type="move"]'); var createprot = prs.filter('[type="create"]'); var protections = [], expirys = []; // set edit protection level if (ctx.protectEdit) { protections.push('edit=' + ctx.protectEdit.level); expirys.push(ctx.protectEdit.expiry); } else if (editprot.length) { protections.push('edit=' + editprot.attr("level")); expirys.push(editprot.attr("expiry").replace("infinity", "indefinite")); } if (ctx.protectMove) { protections.push('move=' + ctx.protectMove.level); expirys.push(ctx.protectMove.expiry); } else if (moveprot.length) { protections.push('move=' + moveprot.attr("level")); expirys.push(moveprot.attr("expiry").replace("infinity", "indefinite")); } if (ctx.protectCreate) { protections.push('create=' + ctx.protectCreate.level); expirys.push(ctx.protectCreate.expiry); } else if (createprot.length) { protections.push('create=' + createprot.attr("level")); expirys.push(createprot.attr("expiry").replace("infinity", "indefinite")); } var query = { action: 'protect', title: $(xml).find('page').attr('title'), token: protectToken, protections: protections.join('|'), expiry: expirys.join('|'), reason: ctx.editSummary }; if (ctx.protectCascade) { query.cascade = 'true'; } if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.protectProcessApi = new Morebits.wiki.api("protecting page...", query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure); ctx.protectProcessApi.setParent(this); ctx.protectProcessApi.post(); }; var fnProcessStabilize = function() { var xml = ctx.stabilizeApi.getXML(); var missing = ($(xml).find('page').attr('missing') === ""); if (missing) { ctx.statusElement.error("Cannot protect the page, because it no longer exists"); ctx.onStabilizeFailure(this); return; } var stabilizeToken = $(xml).find('tokens').attr('csrftoken'); if (!stabilizeToken) { ctx.statusElement.error("Failed to retrieve stabilize token."); ctx.onStabilizeFailure(this); return; } var query = { action: 'stabilize', title: $(xml).find('page').attr('title'), token: stabilizeToken, protectlevel: ctx.flaggedRevs.level, expiry: ctx.flaggedRevs.expiry, reason: ctx.editSummary }; if (ctx.watchlistOption === 'watch') { query.watch = 'true'; } ctx.stabilizeProcessApi = new Morebits.wiki.api("configuring stabilization settings...", query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure); ctx.stabilizeProcessApi.setParent(this); ctx.stabilizeProcessApi.post(); }; }; // end Morebits.wiki.page /** Morebits.wiki.page TODO: (XXX) * - Should we retry loads also? * - Need to reset current action before the save? * - Deal with action.completed stuff * - Need to reset all parameters once done (e.g. edit summary, move destination, etc.) */ /** * **************** Morebits.wiki.preview **************** * Uses the API to parse a fragment of wikitext and render it as HTML. * * Constructor: Morebits.wiki.preview(previewbox, currentAction) * previewbox - the <div> element that will contain the rendered HTML * * beginRender(wikitext): Displays the preview box, and begins an asynchronous attempt * to render the specified wikitext. * wikitext - wikitext to render; most things should work, including subst: and ~~~~ * * closePreview(): Hides the preview box and clears it. * * The suggested implementation pattern (in Morebits.simpleWindow + Morebits.quickForm situations) is to * construct a Morebits.wiki.preview object after rendering a Morebits.quickForm, and bind the object * to an arbitrary property of the form (e.g. |previewer|). For an example, see * twinklewarn.js. */ Morebits.wiki.preview = function(previewbox) { this.previewbox = previewbox; $(previewbox).addClass("morebits-previewbox").hide(); this.beginRender = function(wikitext) { $(previewbox).show(); var statusspan = document.createElement('span'); previewbox.appendChild(statusspan); Morebits.status.init(statusspan); var query = { action: 'parse', prop: 'text', pst: 'true', // PST = pre-save transform; this makes substitution work properly text: wikitext, title: mw.config.get('wgPageName') }; var renderApi = new Morebits.wiki.api("loading...", query, fnRenderSuccess, new Morebits.status("Preview")); renderApi.post(); }; var fnRenderSuccess = function(apiobj) { var xml = apiobj.getXML(); var html = $(xml).find('text').text(); if (!html) { apiobj.statelem.error("failed to retrieve preview, or template was blanked"); return; } previewbox.innerHTML = html; }; this.closePreview = function() { $(previewbox).empty().hide(); }; }; /** * **************** Morebits.wikitext **************** * Wikitext manipulation */ Morebits.wikitext = {}; Morebits.wikitext.template = { parse: function( text, start ) { var count = -1; var level = -1; var equals = -1; var current = ''; var result = { name: '', parameters: {} }; var key, value; for( var i = start; i < text.length; ++i ) { var test3 = text.substr( i, 3 ); if( test3 === '{{{' ) { current += '{{{'; i += 2; ++level; continue; } if( test3 === '}}}' ) { current += '}}}'; i += 2; --level; continue; } var test2 = text.substr( i, 2 ); if( test2 === '{{' || test2 === '[[' ) { current += test2; ++i; ++level; continue; } if( test2 === '[[' ) { current += test2; ++i; --level; continue; } if( test2 === '}}' ) { current += test2; ++i; --level; if( level <= 0 ) { if( count === -1 ) { result.name = current.substring(2).trim(); ++count; } else { if( equals !== -1 ) { key = current.substring( 0, equals ).trim(); value = current.substring( equals ).trim(); result.parameters[key] = value; equals = -1; } else { result.parameters[count] = current; ++count; } } break; } continue; } if( text.charAt(i) === '|' && level <= 0 ) { if( count === -1 ) { result.name = current.substring(2).trim(); ++count; } else { if( equals !== -1 ) { key = current.substring( 0, equals ).trim(); value = current.substring( equals + 1 ).trim(); result.parameters[key] = value; equals = -1; } else { result.parameters[count] = current; ++count; } } current = ''; } else if( equals === -1 && text.charAt(i) === '=' && level <= 0 ) { equals = current.length; current += text.charAt(i); } else { current += text.charAt(i); } } return result; } }; Morebits.wikitext.page = function mediawikiPage( text ) { this.text = text; }; Morebits.wikitext.page.prototype = { text: '', removeLink: function( link_target ) { var first_char = link_target.substr( 0, 1 ); var link_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( link_target.substr( 1 ), true ); var link_simple_re = new RegExp( "\\[\\[:?(" + link_re_string + ")\\]\\]", 'g' ); var link_named_re = new RegExp( "\\[\\[:?" + link_re_string + "\\|(.+?)\\]\\]", 'g' ); this.text = this.text.replace( link_simple_re, "$1" ).replace( link_named_re, "$1" ); }, commentOutImage: function( image, reason ) { var unbinder = new Morebits.unbinder( this.text ); unbinder.unbind( '<!--', '-->' ); reason = reason ? (reason + ': ') : ''; var first_char = image.substr( 0, 1 ); var image_re_string = "[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( image.substr( 1 ), true ); /* * Check for normal image links, i.e. [[Image:Foobar.png|...]] * Will eat the whole link */ var links_re = new RegExp( "\\[\\[(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string ); var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( unbinder.content, '[[', ']]' )); for( var i = 0; i < allLinks.length; ++i ) { if( links_re.test( allLinks[i] ) ) { var replacement = '<!-- ' + reason + allLinks[i] + ' -->'; unbinder.content = unbinder.content.replace( allLinks[i], replacement, 'g' ); } } // unbind the newly created comments unbinder.unbind( '<!--', '-->' ); /* * Check for gallery images, i.e. instances that must start on a new line, eventually preceded with some space, and must include Image: prefix * Will eat the whole line. */ var gallery_image_re = new RegExp( "(^\\s*(?:[Ii]mage|[Ff]ile):\\s*" + image_re_string + ".*?$)", 'mg' ); unbinder.content.replace( gallery_image_re, "<!-- " + reason + "$1 -->" ); // unbind the newly created comments unbinder.unbind( '<!--', '-->' ); /* * Check free image usages, for example as template arguments, might have the Image: prefix excluded, but must be preceeded by an | * Will only eat the image name and the preceeding bar and an eventual named parameter */ var free_image_re = new RegExp( "(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:(?:[Ii]mage|[Ff]ile):\\s*)?" + image_re_string + ")", 'mg' ); unbinder.content.replace( free_image_re, "<!-- " + reason + "$1 -->" ); // Rebind the content now, we are done! this.text = unbinder.rebind(); }, addToImageComment: function( image, data ) { var first_char = image.substr( 0, 1 ); var first_char_regex = RegExp.escape( first_char, true ); if( first_char.toUpperCase() !== first_char.toLowerCase() ) { first_char_regex = '[' + RegExp.escape( first_char.toUpperCase(), true ) + RegExp.escape( first_char.toLowerCase(), true ) + ']'; } var image_re_string = "(?:[Ii]mage|[Ff]ile):\\s*" + first_char_regex + RegExp.escape( image.substr( 1 ), true ); var links_re = new RegExp( "\\[\\[" + image_re_string ); var allLinks = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '[[', ']]' )); for( var i = 0; i < allLinks.length; ++i ) { if( links_re.test( allLinks[i] ) ) { var replacement = allLinks[i]; // just put it at the end? replacement = replacement.replace( /\]\]$/, '|' + data + ']]' ); this.text = this.text.replace( allLinks[i], replacement, 'g' ); } } var gallery_re = new RegExp( "^(\\s*" + image_re_string + '.*?)\\|?(.*?)$', 'mg' ); var newtext = "$1|$2 " + data; this.text = this.text.replace( gallery_re, newtext ); }, removeTemplate: function( template ) { var first_char = template.substr( 0, 1 ); var template_re_string = "(?:[Tt]emplate:)?\\s*[" + first_char.toUpperCase() + first_char.toLowerCase() + ']' + RegExp.escape( template.substr( 1 ), true ); var links_re = new RegExp( "\\{\\{" + template_re_string ); var allTemplates = Morebits.array.uniq(Morebits.string.splitWeightedByKeys( this.text, '{{', '}}', [ '{{{', '}}}' ] )); for( var i = 0; i < allTemplates.length; ++i ) { if( links_re.test( allTemplates[i] ) ) { this.text = this.text.replace( allTemplates[i], '', 'g' ); } } }, getText: function() { return this.text; } }; /** * **************** Morebits.queryString **************** * Maps the querystring to an object * * Functions: * * Morebits.queryString.exists(key) * returns true if the particular key is set * Morebits.queryString.get(key) * returns the value associated to the key * Morebits.queryString.equals(key, value) * returns true if the value associated with given key equals given value * Morebits.queryString.toString() * returns the query string as a string * Morebits.queryString.create( hash ) * creates an querystring and encodes strings via encodeURIComponent and joins arrays with | * * In static context, the value of location.search.substring(1), else the value given to the constructor is going to be used. The mapped hash is saved in the object. * * Example: * * var value = Morebits.queryString.get('key'); * var obj = new Morebits.queryString('foo=bar&baz=quux'); * value = obj.get('foo'); */ Morebits.queryString = function QueryString(qString) { this.string = qString; this.params = {}; if( !qString.length ) { return; } qString.replace(/\+/, ' '); var args = qString.split('&'); for( var i = 0; i < args.length; ++i ) { var pair = args[i].split( '=' ); var key = decodeURIComponent( pair[0] ), value = key; if( pair.length === 2 ) { value = decodeURIComponent( pair[1] ); } this.params[key] = value; } }; Morebits.queryString.staticstr = null; Morebits.queryString.staticInit = function() { if( !Morebits.queryString.staticstr ) { Morebits.queryString.staticstr = new Morebits.queryString(location.search.substring(1)); } }; Morebits.queryString.get = function(key) { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.get(key); }; Morebits.queryString.prototype.get = function(key) { return this.params[key] ? this.params[key] : null; }; Morebits.queryString.exists = function(key) { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.exists(key); }; Morebits.queryString.prototype.exists = function(key) { return this.params[key] ? true : false; }; Morebits.queryString.equals = function(key, value) { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.equals(key, value); }; Morebits.queryString.prototype.equals = function(key, value) { return this.params[key] === value ? true : false; }; Morebits.queryString.toString = function() { Morebits.queryString.staticInit(); return Morebits.queryString.staticstr.toString(); }; Morebits.queryString.prototype.toString = function() { return this.string ? this.string : null; }; Morebits.queryString.create = function( arr ) { var resarr = []; var editToken; // KLUGE: this should always be the last item in the query string (bug TW-B-0013) for( var i in arr ) { if( arr[i] === undefined ) { continue; } var res; if( $.isArray( arr[i] ) ){ var v = []; for(var j = 0; j < arr[i].length; ++j ) { v[j] = encodeURIComponent( arr[i][j] ); } res = v.join('|'); } else { res = encodeURIComponent( arr[i] ); } if( i === 'token' ) { editToken = res; } else { resarr.push( encodeURIComponent( i ) + '=' + res ); } } if( editToken !== undefined ) { resarr.push( 'token=' + editToken ); } return resarr.join('&'); }; Morebits.queryString.prototype.create = Morebits.queryString.create; /** * **************** Morebits.status **************** */ Morebits.status = function Status( text, stat, type ) { this.textRaw = text; this.text = this.codify(text); this.type = type || 'status'; this.generate(); if( stat ) { this.update( stat, type ); } }; Morebits.status.init = function( root ) { if( !( root instanceof Element ) ) { throw new Error( 'object not an instance of Element' ); } while( root.hasChildNodes() ) { root.removeChild( root.firstChild ); } Morebits.status.root = root; Morebits.status.errorEvent = null; }; Morebits.status.root = null; Morebits.status.onError = function( handler ) { if ( $.isFunction( handler ) ) { Morebits.status.errorEvent = handler; } else { throw "Morebits.status.onError: handler is not a function"; } }; Morebits.status.prototype = { stat: null, text: null, textRaw: null, type: 'status', target: null, node: null, linked: false, link: function() { if( ! this.linked && Morebits.status.root ) { Morebits.status.root.appendChild( this.node ); this.linked = true; } }, unlink: function() { if( this.linked ) { Morebits.status.root.removeChild( this.node ); this.linked = false; } }, codify: function( obj ) { if ( ! $.isArray( obj ) ) { obj = [ obj ]; } var result; result = document.createDocumentFragment(); for( var i = 0; i < obj.length; ++i ) { if( typeof obj[i] === 'string' ) { result.appendChild( document.createTextNode( obj[i] ) ); } else if( obj[i] instanceof Element ) { result.appendChild( obj[i] ); } // Else cosmic radiation made something shit } return result; }, update: function( status, type ) { this.stat = this.codify( status ); if( type ) { this.type = type; if (type === 'error') { // hack to force the page not to reload when an error is output - see also Morebits.status() above Morebits.wiki.numberOfActionsLeft = 1000; // call error callback if (Morebits.status.errorEvent) { Morebits.status.errorEvent(); } // also log error messages in the browser console if (console && console.error) { console.error(this.textRaw + ": " + status); } } } this.render(); }, generate: function() { this.node = document.createElement( 'div' ); this.node.appendChild( document.createElement('span') ).appendChild( this.text ); this.node.appendChild( document.createElement('span') ).appendChild( document.createTextNode( ': ' ) ); this.target = this.node.appendChild( document.createElement( 'span' ) ); this.target.appendChild( document.createTextNode( '' ) ); // dummy node }, render: function() { this.node.className = 'tw_status_' + this.type; while( this.target.hasChildNodes() ) { this.target.removeChild( this.target.firstChild ); } this.target.appendChild( this.stat ); this.link(); }, status: function( status ) { this.update( status, 'status'); }, info: function( status ) { this.update( status, 'info'); }, warn: function( status ) { this.update( status, 'warn'); }, error: function( status ) { this.update( status, 'error'); } }; Morebits.status.info = function( text, status ) { return new Morebits.status( text, status, 'info' ); }; Morebits.status.warn = function( text, status ) { return new Morebits.status( text, status, 'warn' ); }; Morebits.status.error = function( text, status ) { return new Morebits.status( text, status, 'error' ); }; /** * **************** Morebits.htmlNode() **************** * Simple helper function to create a simple node */ Morebits.htmlNode = function ( type, content, color ) { var node = document.createElement( type ); if( color ) { node.style.color = color; } node.appendChild( document.createTextNode( content ) ); return node; } /** * **************** Morebits.simpleWindow **************** * A simple draggable window * now a wrapper for jQuery UI's dialog feature */ // The height passed in here is the maximum allowable height for the content area. Morebits.simpleWindow = function SimpleWindow( width, height ) { var content = document.createElement( 'div' ); this.content = content; content.className = 'morebits-dialog-content'; this.height = height; $(this.content).dialog({ autoOpen: false, buttons: { "Placeholder button": function() {} }, dialogClass: 'morebits-dialog', width: Math.min(parseInt(window.innerWidth, 10), parseInt(width ? width : 800, 10)), // give jQuery the given height value (which represents the anticipated height of the dialog) here, so // it can position the dialog appropriately // the 20 pixels represents adjustment for the extra height of the jQuery dialog "chrome", compared // to that of the old SimpleWindow height: height + 20, close: function(event, ui) { // dialogs and their content can be destroyed once closed $(event.target).dialog("destroy").remove(); }, resize: function(event, ui) { this.style.maxHeight = ""; } }); var $widget = $(this.content).dialog("widget"); // add background gradient to titlebar var $titlebar = $widget.find(".ui-dialog-titlebar"); var oldstyle = $titlebar.attr("style"); $titlebar.attr("style", (oldstyle ? oldstyle : "") + '; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC) !important;'); // delete the placeholder button (it's only there so the buttonpane gets created) $widget.find("button").each(function(key, value) { value.parentNode.removeChild(value); }); // add container for the buttons we add, and the footer links (if any) var buttonspan = document.createElement("span"); buttonspan.className = "morebits-dialog-buttons"; var linksspan = document.createElement("span"); linksspan.className = "morebits-dialog-footerlinks"; $widget.find(".ui-dialog-buttonpane").append(buttonspan, linksspan); }; Morebits.simpleWindow.prototype = { buttons: [], height: 600, hasFooterLinks: false, scriptName: null, // Focuses the dialog. This might work, or on the contrary, it might not. focus: function(event) { $(this.content).dialog("moveToTop"); return this; }, // Closes the dialog. If this is set as an event handler, it will stop the event from doing anything more. close: function(event) { if (event) { event.preventDefault(); } $(this.content).dialog("close"); return this; }, // Shows the dialog. Calling display() on a dialog that has previously been closed might work, but it is not guaranteed. display: function() { if (this.scriptName) { var $widget = $(this.content).dialog("widget"); $widget.find(".morebits-dialog-scriptname").remove(); var scriptnamespan = document.createElement("span"); scriptnamespan.className = "morebits-dialog-scriptname"; scriptnamespan.textContent = this.scriptName + " \u00B7 "; // U+00B7 MIDDLE DOT = &middot; $widget.find(".ui-dialog-title").prepend(scriptnamespan); } var dialog = $(this.content).dialog("open"); if (window.setupTooltips && window.pg && window.pg.re && window.pg.re.diff) { // tie in with NAVPOP dialog.parent()[0].ranSetupTooltipsAlready = false; setupTooltips(dialog.parent()[0]); } this.setHeight( this.height ); // init height algorithm return this; }, // Sets the dialog title. setTitle: function( title ) { $(this.content).dialog("option", "title", title); return this; }, // Sets the script name, appearing as a prefix to the title to help users determine which // user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle". setScriptName: function( name ) { this.scriptName = name; return this; }, // Sets the dialog width. setWidth: function( width ) { $(this.content).dialog("option", "width", width); return this; }, // Sets the dialog's maximum height. The dialog will auto-size to fit its contents, // but the content area will grow no larger than the height given here. setHeight: function( height ) { this.height = height; // from display time onwards, let the browser determine the optimum height, and instead limit the height at the given value // note that the given height will exclude the approx. 20px that the jQuery UI chrome has in height in addition to the height // of an equivalent "classic" Morebits.simpleWindow if (parseInt(getComputedStyle($(this.content).dialog("widget")[0], null).height, 10) > window.innerHeight) { $(this.content).dialog("option", "height", window.innerHeight - 2).dialog("option", "position", "top"); } else { $(this.content).dialog("option", "height", "auto"); } $(this.content).dialog("widget").find(".morebits-dialog-content")[0].style.maxHeight = parseInt(this.height - 30, 10) + "px"; return this; }, // Sets the content of the dialog to the given element node, usually from rendering a Morebits.quickForm. // Re-enumerates the footer buttons, but leaves the footer links as they are. // Be sure to call this at least once before the dialog is displayed... setContent: function( content ) { this.purgeContent(); this.addContent( content ); return this; }, addContent: function( content ) { this.content.appendChild( content ); // look for submit buttons in the content, hide them, and add a proxy button to the button pane var thisproxy = this; $(this.content).find('input[type="submit"], button[type="submit"]').each(function(key, value) { value.style.display = "none"; var button = document.createElement("button"); button.textContent = (value.hasAttribute("value") ? value.getAttribute("value") : (value.textContent ? value.textContent : "Submit Query")); // here is an instance of cheap coding, probably a memory-usage hit in using a closure here button.addEventListener("click", function() { value.click(); }, false); thisproxy.buttons.push(button); }); // remove all buttons from the button pane and re-add them if (this.buttons.length > 0) { $(this.content).dialog("widget").find(".morebits-dialog-buttons").empty().append(this.buttons)[0].removeAttribute("data-empty"); } else { $(this.content).dialog("widget").find(".morebits-dialog-buttons")[0].setAttribute("data-empty", "data-empty"); // used by CSS } return this; }, purgeContent: function( content ) { this.buttons = []; // delete all buttons in the buttonpane $(this.content).dialog("widget").find(".morebits-dialog-buttons").empty(); while( this.content.hasChildNodes() ) { this.content.removeChild( this.content.firstChild ); } return this; }, // Adds a link in the bottom-right corner of the dialog. // This can be used to provide help or policy links. // For example, Twinkle's CSD module adds a link to the CSD policy page, // as well as a link to Twinkle's documentation. addFooterLink: function( text, wikiPage ) { var $footerlinks = $(this.content).dialog("widget").find(".morebits-dialog-footerlinks"); if (this.hasFooterLinks) { var bullet = document.createElement("span"); bullet.textContent = " \u2022 "; // U+2022 BULLET $footerlinks.append(bullet); } var link = document.createElement("a"); link.setAttribute("href", mw.util.getUrl(wikiPage) ); link.setAttribute("title", wikiPage); link.setAttribute("target", "_blank"); link.textContent = text; $footerlinks.append(link); this.hasFooterLinks = true; return this; }, setModality: function( modal ) { $(this.content).dialog("option", "modal", modal); return this; } }; // Enables or disables all footer buttons on all Morebits.simpleWindows in the current page. // This should be called with |false| when the button(s) become irrelevant (e.g. just before Morebits.status.init is called). // This is not an instance method so that consumers don't have to keep a reference to the original // Morebits.simpleWindow object sitting around somewhere. Anyway, most of the time there will only be one // Morebits.simpleWindow open, so this shouldn't matter. Morebits.simpleWindow.setButtonsEnabled = function( enabled ) { $(".morebits-dialog-buttons button").attr("disabled", !enabled); }; // Twinkle blacklist was removed per consensus at http://en.wikipedia.org/wiki/Wikipedia:Administrators%27_noticeboard/Archive221#New_Twinkle_blacklist_proposal } ( window, document, jQuery )); // End wrap with anonymous function /** * If this script is being executed outside a ResourceLoader context, we add some * global assignments for legacy scripts, hopefully these can be removed down the line * * IMPORTANT NOTE: * PLEASE DO NOT USE THESE ALIASES IN NEW CODE! * Thanks. */ if ( typeof arguments === "undefined" ) { // typeof is here for a reason... window.SimpleWindow = Morebits.simpleWindow; window.QuickForm = Morebits.quickForm; window.Wikipedia = Morebits.wiki; window.Status = Morebits.status; window.QueryString = Morebits.queryString; } // </nowiki> // [[Category:Scripts]] // [[Category:Twinkle]] be7ce576a8bd7447153c70167bdc568d1cc37459 MediaWiki:Gadget-Twinkle.js 8 83 159 158 2023-08-28T02:15:25Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 javascript text/javascript /** Twinkle.js [[Category:Twinkle]] Forked from simplewiki's version of Twinkle and de-Wikipedia-fied by Naleksuh Currently in beta and still has some reseblences to Wikipedia. Will be more fine-tuned over time */ //<nowiki> mw.loader.load("https://dev.miraheze.org/w/index.php?title=MediaWiki:Jquerymigrate-3.3.2.js&action=raw&ctype=text/javascript"); ( function ( window, document, $, undefined ) { // Wrap with anonymous function var Twinkle = {}; window.Twinkle = Twinkle; // allow global access // for use by custom modules (normally empty) Twinkle.initCallbacks = []; Twinkle.addInitCallback = function twinkleAddInitCallback( func ) { Twinkle.initCallbacks.push( func ); }; Twinkle.defaultConfig = {}; /** * Twinkle.defaultConfig.twinkle and Twinkle.defaultConfig.friendly * * This holds the default set of preferences used by Twinkle. (The |friendly| object holds preferences stored in the FriendlyConfig object.) * It is important that all new preferences added here, especially admin-only ones, are also added to * |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel. * For help on the actual preferences, see the comments in twinkleconfig.js. */ Twinkle.defaultConfig.twinkle = { // General summaryAd: " ([[mh:dev:Twinkle|TW]])", deletionSummaryAd: " ([[mh:dev:Twinkle|TW]])", protectionSummaryAd: " ([[mh:dev:Twinkle|TW]])", userTalkPageMode: "window", dialogLargeFont: false, // Fluff (revert and rollback) openTalkPage: [ "agf", "norm", "vand" ], openTalkPageOnAutoRevert: false, markRevertedPagesAsMinor: [ "vand" ], watchRevertedPages: [ "agf", "norm", "vand", "torev" ], offerReasonOnNormalRevert: true, confirmOnFluff: false, showRollbackLinks: [ "diff", "others" ], // CSD speedySelectionStyle: "buttonClick", speedyPromptOnG7: false, watchSpeedyPages: [ "g3", "g5", "g10", "g11", "g12" ], markSpeedyPagesAsPatrolled: true, // these next two should probably be identical by default notifyUserOnSpeedyDeletionNomination: [ ], welcomeUserOnSpeedyDeletionNotification: [ ], promptForSpeedyDeletionSummary: [ "db" ], openUserTalkPageOnSpeedyDelete: [ "db"], deleteTalkPageOnDelete: false, deleteSysopDefaultToTag: false, speedyWindowHeight: 500, speedyWindowWidth: 800, logSpeedyNominations: false, speedyLogPageName: "Deletion request log", noLogOnSpeedyNomination: [ "u1" ], // Unlink unlinkNamespaces: [ "0" ], // Warn defaultWarningGroup: "1", showSharedIPNotice: true, watchWarnings: true, blankTalkpageOnIndefBlock: false, // XfD xfdWatchDiscussion: "default", xfdWatchList: "no", xfdWatchPage: "default", xfdWatchUser: "default", // Hidden preferences revertMaxRevisions: 50, batchdeleteChunks: 50, batchDeleteMinCutOff: 5, batchMax: 5000, batchProtectChunks: 50, batchProtectMinCutOff: 5, batchundeleteChunks: 50, batchUndeleteMinCutOff: 5 }; // now some skin dependent config. if ( mw.config.get( "skin" ) === "vector" || mw.config.get("skin") === "vector-2022") { Twinkle.defaultConfig.twinkle.portletArea = "right-navigation"; Twinkle.defaultConfig.twinkle.portletId = "p-twinkle"; Twinkle.defaultConfig.twinkle.portletName = "TW"; Twinkle.defaultConfig.twinkle.portletType = "menu"; Twinkle.defaultConfig.twinkle.portletNext = "p-search"; } else { Twinkle.defaultConfig.twinkle.portletArea = null; Twinkle.defaultConfig.twinkle.portletId = "p-cactions"; Twinkle.defaultConfig.twinkle.portletName = null; Twinkle.defaultConfig.twinkle.portletType = null; Twinkle.defaultConfig.twinkle.portletNext = null; } Twinkle.defaultConfig.friendly = { // Tag groupByDefault: true, watchTaggedPages: true, markTaggedPagesAsMinor: false, markTaggedPagesAsPatrolled: true, tagArticleSortOrder: "cat", customTagList: [], // Stub watchStubbedPages: true, markStubbedPagesAsMinor: false, markStubbedPagesAsPatrolled: true, stubArticleSortOrder: "cat", // Welcome topWelcomes: false, watchWelcomes: true, welcomeHeading: "Welcome", insertHeadings: true, insertUsername: true, insertSignature: true, // sign welcome templates, where appropriate quickWelcomeMode: "norm", quickWelcomeTemplate: "welcome", customWelcomeList: [], // Talkback markTalkbackAsMinor: true, insertTalkbackSignature: true, // always sign talkback templates talkbackHeading: "Talkback", adminNoticeHeading: "Notice", mailHeading: "You've got mail!", // Shared markSharedIPAsMinor: true }; Twinkle.getPref = function twinkleGetPref( name ) { var result; if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.twinkle === "object" ) { // look in Twinkle.prefs (twinkleoptions.js) result = Twinkle.prefs.twinkle[name]; } else if ( typeof window.TwinkleConfig === "object" ) { // look in TwinkleConfig result = window.TwinkleConfig[name]; } if ( result === undefined ) { return Twinkle.defaultConfig.twinkle[name]; } return result; }; Twinkle.getFriendlyPref = function twinkleGetFriendlyPref(name) { var result; if ( typeof Twinkle.prefs === "object" && typeof Twinkle.prefs.friendly === "object" ) { // look in Twinkle.prefs (twinkleoptions.js) result = Twinkle.prefs.friendly[ name ]; } else if ( typeof window.FriendlyConfig === "object" ) { // look in FriendlyConfig result = window.FriendlyConfig[ name ]; } if ( result === undefined ) { return Twinkle.defaultConfig.friendly[ name ]; } return result; }; /** * **************** twAddPortlet() **************** * * Adds a portlet menu to one of the navigation areas on the page. * This is necessarily quite a hack since skins, navigation areas, and * portlet menu types all work slightly different. * * Available navigation areas depend on the skin used. * Vector: * For each option, the outer div class contains "vector-menu", the inner div class is "vector-menu-content", and the ul is "vector-menu-content-list" * "mw-panel", outer div class contains "vector-menu-portal". Existing portlets/elements: "p-logo", "p-navigation", "p-interaction", "p-tb", "p-coll-print_export" * "left-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-namespaces", "p-variants" (menu) * "right-navigation", outer div class contains "vector-menu-tabs" or "vector-menu-dropdown". Existing portlets: "p-views", "p-cactions" (menu), "p-search" * Special layout of p-personal portlet (part of "head") through specialized styles. * Monobook: * "column-one", outer div class "portlet", inner div class "pBody". Existing portlets: "p-cactions", "p-personal", "p-logo", "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export" * Special layout of p-cactions and p-personal through specialized styles. * Modern: * "mw_contentwrapper" (top nav), outer div class "portlet", inner div class "pBody". Existing portlets or elements: "p-cactions", "mw_content" * "mw_portlets" (sidebar), outer div class "portlet", inner div class "pBody". Existing portlets: "p-navigation", "p-search", "p-interaction", "p-tb", "p-coll-print_export" * * @param String navigation -- id of the target navigation area (skin dependant, on vector either of "left-navigation", "right-navigation", or "mw-panel") * @param String id -- id of the portlet menu to create, preferably start with "p-". * @param String text -- name of the portlet menu to create. Visibility depends on the class used. * @param String type -- type of portlet. Currently only used for the vector non-sidebar portlets, pass "menu" to make this portlet a drop down menu. * @param Node nextnodeid -- the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end. * * @return Node -- the DOM node of the new item (a DIV element) or null */ function twAddPortlet( navigation, id, text, type, nextnodeid ) { //sanity checks, and get required DOM nodes var root = document.getElementById( navigation ); if ( !root ) { return null; } var item = document.getElementById( id ); if ( item ) { if ( item.parentNode && item.parentNode === root ) { return item; } return null; } var nextnode; if ( nextnodeid ) { nextnode = document.getElementById(nextnodeid); } if ((mw.config.get('skin') !== 'vector' && mw.config.get('skin') !== 'vector-2022') || (navigation !== 'left-navigation' && navigation !== 'right-navigation')) { type = null; // menu supported only in vector's #left-navigation & #right-navigation } var outerDivClass; var innerDivClass; switch (mw.config.get('skin')) { case "vector": case 'vector-2022': if ( navigation !== "portal" && navigation !== "left-navigation" && navigation !== "right-navigation" ) { navigation = "mw-panel"; } outerDivClass = 'vector-menu vector-menu-' + (navigation === 'mw-panel' ? 'portal' : type === 'menu' ? 'dropdown vector-menu-dropdown-noicon' : 'tabs'); innerDivClass = 'vector-menu-content'; break; case "modern": if ( navigation !== "mw_portlets" && navigation !== "mw_contentwrapper" ) { navigation = "mw_portlets"; } outerDivClass = "portlet"; innerDivClass = "pBody"; break; default: navigation = "column-one"; outerDivClass = "portlet"; innerDivClass = "pBody"; break; } // Build the DOM elements. var outerDiv = document.createElement('nav'); outerDiv.setAttribute('aria-labelledby', id + '-label'); // Vector getting vector-menu-empty FIXME TODO outerDiv.className = outerDivClass + ' emptyPortlet'; outerDiv.id = id; if (nextnode && nextnode.parentNode === root) { root.insertBefore(outerDiv, nextnode); } else { root.appendChild(outerDiv); } var h3 = document.createElement('h3'); h3.id = id + '-label'; var ul = document.createElement('ul'); if (mw.config.get( "skin" ) === 'vector' || mw.config.get("skin") === 'vector-2022') { h3.className = "vector-menu-heading"; // add invisible checkbox to keep menu open when clicked // similar to the p-cactions ("More") menu if (outerDivClass.indexOf('vector-menu-dropdown') !== -1) { var chkbox = document.createElement('input'); chkbox.className = 'vectorMenuCheckbox vector-menu-checkbox'; // remove vectorMenuCheckbox after 1.35-wmf.37 goes live chkbox.setAttribute('type', 'checkbox'); chkbox.setAttribute('aria-labelledby', id + '-label'); outerDiv.appendChild(chkbox); var span = document.createElement('span'); span.appendChild(document.createTextNode(text)); h3.appendChild(span); var a = document.createElement('a'); a.href = '#'; $(a).click(function(e) { e.preventDefault(); }); h3.appendChild(a); } outerDiv.appendChild(h3); ul.className = 'menu vector-menu-content-list'; // remove menu after 1.35-wmf.37 goes live } else { h3.appendChild(document.createTextNode(text)); outerDiv.appendChild(h3); } if (innerDivClass) { var innerDiv = document.createElement('div'); innerDiv.className = innerDivClass; innerDiv.appendChild(ul); outerDiv.appendChild(innerDiv); } else { outerDiv.appendChild(ul); } return outerDiv; } /** * **************** twAddPortletLink() **************** * Builds a portlet menu if it doesn't exist yet, and add the portlet link. * @param task: Either a URL for the portlet link or a function to execute. */ function twAddPortletLink( task, text, id, tooltip ) { if ( Twinkle.getPref("portletArea") !== null ) { twAddPortlet( Twinkle.getPref( "portletArea" ), Twinkle.getPref( "portletId" ), Twinkle.getPref( "portletName" ), Twinkle.getPref( "portletType" ), Twinkle.getPref( "portletNext" )); } var link = mw.util.addPortletLink( Twinkle.getPref( "portletId" ), typeof task === "string" ? task : "#", text, id, tooltip ); $('.client-js .skin-vector #p-cactions').css('margin-right', 'initial'); if ( $.isFunction( task ) ) { $( link ).click(function ( ev ) { task(); ev.preventDefault(); }); } if ($.collapsibleTabs) { $.collapsibleTabs.handleResize(); } return link; } // Check if account is experienced enough to use Twinkle var twinkleUserAuthorized = Morebits.userIsInGroup( "autoconfirmed" ) || Morebits.userIsInGroup( "confirmed" ) || Morebits.userIsInGroup( "sysop" ); /* **************************************** *** friendlyshared.js: Shared IP tagging module **************************************** * Mode of invocation: Tab ("Shared") * Active on: Existing IP user talk pages * Config directives in: FriendlyConfig */ Twinkle.shared = function friendlyshared() { if( mw.config.get('wgNamespaceNumber') === 3 && Morebits.isIPAddress(mw.config.get('wgTitle')) ) { var username = mw.config.get('wgTitle').split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes twAddPortletLink( function(){ Twinkle.shared.callback(username); }, "Shared IP", "friendly-shared", "Shared IP tagging" ); } }; Twinkle.shared.callback = function friendlysharedCallback( uid ) { var Window = new Morebits.simpleWindow( 600, 400 ); Window.setTitle( "Shared IP address tagging" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#shared" ); var form = new Morebits.quickForm( Twinkle.shared.callback.evaluate ); var div = form.append( { type: 'div', id: 'sharedip-templatelist' } ); div.append( { type: 'header', label: 'Shared IP address templates' } ); div.append( { type: 'radio', name: 'shared', list: Twinkle.shared.standardList, event: function( e ) { Twinkle.shared.callback.change_shared( e ); e.stopPropagation(); } } ); var org = form.append( { type:'field', label:'Fill in other details (optional) and click \"Submit\"' } ); org.append( { type: 'input', name: 'organization', label: 'IP address owner/operator', disabled: true, tooltip: 'You can optionally enter the name of the organization that owns/operates the IP address. You can use wikimarkup if necessary.' } ); org.append( { type: 'input', name: 'host', label: 'Host name (optional)', disabled: true, tooltip: 'The host name (for example, proxy.example.com) can be optionally entered here and will be linked by the template.' } ); org.append( { type: 'input', name: 'contact', label: 'Contact information (only if requested)', disabled: true, tooltip: 'You can optionally enter some contact details for the organization. Use this parameter only if the organization has specifically requested that it be added. You can use wikimarkup if necessary.' } ); form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); $(result).find('div#sharedip-templatelist').addClass('quickform-scrollbox'); }; Twinkle.shared.standardList = [ { label: '{{SharedIP}}: standard shared IP address template', value: 'Shared IP', tooltip: 'IP user talk page template that shows helpful information to IP users and those wishing to warn, block or ban them' }, { label: '{{SchoolIP}}: shared IP address template modified for educational institutions', value: 'SchoolIP' }, { label: '{{SharedIPCORP}}: shared IP address template modified for businesses', value: 'SharedIPCORP' }, { label: '{{ISP}}: shared IP address template modified for ISP organizations (specifically proxies)', value: 'ISP' } ]; Twinkle.shared.callback.change_shared = function friendlysharedCallbackChangeShared(e) { if( e.target.value === 'Shared IP edu' ) { e.target.form.contact.disabled = false; } else { e.target.form.contact.disabled = true; } e.target.form.organization.disabled=false; e.target.form.host.disabled=false; }; Twinkle.shared.callbacks = { main: function( pageobj ) { var params = pageobj.getCallbackParameters(); var pageText = pageobj.getPageText(); var found = false; var text = '{{'; for( var i=0; i < Twinkle.shared.standardList.length; i++ ) { var tagRe = new RegExp( '(\\{\\{' + Twinkle.shared.standardList[i].value + '(\\||\\}\\}))', 'im' ); if( tagRe.exec( pageText ) ) { Morebits.status.warn( 'Info', 'Found {{' + Twinkle.shared.standardList[i].value + '}} on the user\'s talk page already...aborting' ); found = true; } } if( found ) { return; } Morebits.status.info( 'Info', 'Will add the shared IP address template to the top of the user\'s talk page.' ); text += params.value + '|' + params.organization; if( params.value === 'shared IP edu' && params.contact !== '') { text += '|' + params.contact; } if( params.host !== '' ) { text += '|host=' + params.host; } text += '}}\n\n'; var summaryText = 'Added {{[[Template:' + params.value + '|' + params.value + ']]}} template.'; pageobj.setPageText(text + pageText); pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markSharedIPAsMinor')); pageobj.setCreateOption('recreate'); pageobj.save(); } }; Twinkle.shared.callback.evaluate = function friendlysharedCallbackEvaluate(e) { var shared = e.target.getChecked( 'shared' ); if( !shared || shared.length <= 0 ) { alert( 'You must select a shared IP address template to use!' ); return; } var value = shared[0]; if( e.target.organization.value === '') { alert( 'You must input an organization for the {{' + value + '}} template!' ); return; } var params = { value: value, organization: e.target.organization.value, host: e.target.host.value, contact: e.target.contact.value }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.shared.callbacks.main); }; /* **************************************** *** friendlytag.js: Tag module **************************************** * Mode of invocation: Tab ("Tag") * Active on: Existing articles; file pages with a corresponding file * which is local (not on Commons); existing subpages of * {Wikipedia|Wikipedia talk}:Articles for creation; * all redirects * Config directives in: FriendlyConfig */ Twinkle.tag = function friendlytag() { // redirect tagging if( Morebits.wiki.isPageRedirect() ) { Twinkle.tag.mode = 'redirect'; //twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Tag redirect" ); } // file tagging else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) { Twinkle.tag.mode = 'file'; } // article/draft article tagging else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) { Twinkle.tag.mode = 'article'; //twAddPortletLink( Twinkle.tag.callback, "Tag", "friendly-tag", "Add maintenance tags to article" ); } }; Twinkle.tag.callback = function friendlytagCallback( uid ) { var Window = new Morebits.simpleWindow( 630, (Twinkle.tag.mode === "article") ? 450 : 400 ); Window.setScriptName( "Twinkle" ); // anyone got a good policy/guideline/info page/instructional page link?? Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#tag" ); var form = new Morebits.quickForm( Twinkle.tag.callback.evaluate ); switch( Twinkle.tag.mode ) { case 'article': Window.setTitle( "Article maintenance tagging" ); form.append( { type: 'checkbox', list: [ { label: 'Group inside {{multiple issues}} if possible', value: 'group', name: 'group', tooltip: 'If applying three or more templates supported by {{multiple issues}} and this box is checked, all supported templates will be grouped inside a {{multiple issues}} template.', checked: Twinkle.getFriendlyPref('groupByDefault') } ] } ); form.append({ type: 'select', name: 'sortorder', label: 'View this list:', tooltip: 'You can change the default view order in your Twinkle preferences (mh:dev:Twinkle/Preferences).', event: Twinkle.tag.updateSortOrder, list: [ { type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'cat' }, { type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('tagArticleSortOrder') === 'alpha' } ] }); form.append( { type: 'div', id: 'tagWorkArea' } ); if( Twinkle.getFriendlyPref('customTagList').length ) { form.append( { type: 'header', label: 'Custom tags' } ); form.append( { type: 'checkbox', name: 'articleTags', list: Twinkle.getFriendlyPref('customTagList') } ); } break; case 'redirect': Window.setTitle( "Redirect tagging" ); //Spelling, misspelling, tense and capitalization templates form.append({ type: 'header', label:'All templates' }); form.append({ type: 'checkbox', name: 'redirectTags', list: Twinkle.tag.spellingList }); break; default: alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode); break; } form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); if (Twinkle.tag.mode === "article") { // fake a change event on the sort dropdown, to initialize the tag list var evt = document.createEvent("Event"); evt.initEvent("change", true, true); result.sortorder.dispatchEvent(evt); } }; Twinkle.tag.checkedTags = []; Twinkle.tag.updateSortOrder = function(e) { var sortorder = e.target.value; var $workarea = $(e.target.form).find("div#tagWorkArea"); Twinkle.tag.checkedTags = e.target.form.getChecked("articleTags"); if (!Twinkle.tag.checkedTags) { Twinkle.tag.checkedTags = []; } // function to generate a checkbox, with appropriate subgroup if needed var makeCheckbox = function(tag, description) { var checkbox = { value: tag, label: "{{" + tag + "}}: " + description }; if (Twinkle.tag.checkedTags.indexOf(tag) !== -1) { checkbox.checked = true; } if (tag === "notability") { checkbox.subgroup = { name: 'notability', type: 'select', list: [ { label: "{{notability}}: article\'s subject may not meet the general notability guideline", value: "none" }, { label: "{{notability|Academics}}: notability guideline for academics", value: "Academics" }, { label: "{{notability|Biographies}}: notability guideline for biographies", value: "Biographies" }, { label: "{{notability|Books}}: notability guideline for books", value: "Books" }, { label: "{{notability|Companies}}: notability guidelines for companies and organizations", value: "Companies" }, { label: "{{notability|Events}}: notability guideline for events", value: "Events" }, { label: "{{notability|Films}}: notability guideline for films", value: "Films" }, { label: "{{notability|Music}}: notability guideline for music", value: "Music" }, { label: "{{notability|Neologisms}}: notability guideline for neologisms", value: "Neologisms" }, { label: "{{notability|Numbers}}: notability guideline for numbers", value: "Numbers" }, { label: "{{notability|Products}}: notability guideline for products and services", value: "Products" }, { label: "{{notability|Sport}}: notability guideline for sports and athletics", value: "Sport" }, { label: "{{notability|Web}}: notability guideline for web content", value: "Web" } ] }; } return checkbox; }; // categorical sort order if (sortorder === "cat") { var div = new Morebits.quickForm.element({ type: "div", id: "tagWorkArea" }); // function to iterate through the tags and create a checkbox for each one var doCategoryCheckboxes = function(subdiv, array) { var checkboxes = []; $.each(array, function(k, tag) { var description = Twinkle.tag.article.tags[tag]; checkboxes.push(makeCheckbox(tag, description)); }); subdiv.append({ type: "checkbox", name: "articleTags", list: checkboxes }); }; var i = 0; // go through each category and sub-category and append lists of checkboxes $.each(Twinkle.tag.article.tagCategories, function(title, content) { div.append({ type: "header", id: "tagHeader" + i, label: title }); var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ }); if ($.isArray(content)) { doCategoryCheckboxes(subdiv, content); } else { $.each(content, function(subtitle, subcontent) { subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] }); doCategoryCheckboxes(subdiv, subcontent); }); } }); var rendered = div.render(); $workarea.replaceWith(rendered); var $rendered = $(rendered); $rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' }); $rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' }); } // alphabetical sort order else { var checkboxes = []; $.each(Twinkle.tag.article.tags, function(tag, description) { checkboxes.push(makeCheckbox(tag, description)); }); var tags = new Morebits.quickForm.element({ type: "checkbox", name: "articleTags", list: checkboxes }); $workarea.empty().append(tags.render()); } }; // Tags for ARTICLES start here Twinkle.tag.article = {}; // A list of all article tags, in alphabetical order // To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below. Twinkle.tag.article.tags = { "advertisement": "article is written like an advertisement", "autobiography": "article is an autobiography and may not be written neutrally", "BLP sources": "BLP article needs more sources for verification", "BLP unsourced": "BLP article has no sources at all", "citation style": "article has unclear or inconsistent inline citations", "cleanup": "article may require cleanup", "COI": "article creator or major contributor may have a conflict of interest", "complex": "the English used in this article or section may not be easy for everybody to understand", "confusing": "article may be confusing or unclear", "context": "article provides insufficient context", "copyedit": "article needs copy editing for grammar, style, cohesion, tone, and/or spelling", "dead end": "article has few or no links to other articles", "disputed": "article has questionable factual accuracy", "expert-subject": "article needs attention from an expert on the subject", "external links": "article's external links may not follow content policies or guidelines", "fansite": "article resembles a fansite", "fiction": "article fails to distinguish between fact and fiction", "globalise": "article may not represent a worldwide view of the subject", "hoax": "article may be a complete hoax", "in-universe": "article subject is fictional and needs rewriting from a non-fictional perspective", "in use": "article is undergoing a major edit for a short while", "intro-missing": "article has no lead section and one should be written", "intro-rewrite": "article lead section needs to be rewritten", "intro-tooshort": "article lead section is too short and should be expanded", "jargon": "article uses technical words that not everybody will know", "link rot": "article uses bare URLs for references, which are prone to link rot", "merge": "article should be merged with another given article", "metricate": "article exclusively uses non-SI units of measurement", "more footnotes": "article has some references, but insufficient in-text citations", "more sources": "article needs more sources for verification", "no footnotes": "article has references, but no in-text citations", "no sources": "article has no references at all", "notability": "article's subject may not meet the notability guideline", "NPOV": "article does not maintain a neutral point of view", "one source": "article relies largely or entirely upon a single source", "original research": "article has original research or unverified claims", "orphan": "article is linked to from no other articles", "plot": "plot summary in article is too long", "primary sources": "article relies too heavily on first-hand sources, and needs third-party sources", "prose": "article is in a list format that may be better presented using prose", "redlinks": "article may have too many red links", "restructure": "article may be in need of reorganization to comply with Wikipedia's layout guidelines", "rough translation": "article is poorly translated and needs cleanup", "sections": "article needs to be broken into sections", "self-published": "article may contain improper references to self-published sources", "tone": "tone of article is not appropriate", "uncat": "article is uncategorized", "under construction": "article is currently in the middle of an expansion or major revamping", "unreliable sources": "article's references may not be reliable sources", "update": "article needs additional up-to-date information added", "very long": "article is too long", "weasel": "article neutrality is compromised by the use of weasel words", "wikify": "article needs to be wikified" }; // A list of tags in order of category // Tags should be in alphabetical order within the categories // Add new categories with discretion - the list is long enough as is! Twinkle.tag.article.tagCategories = { "Cleanup and maintenance tags": { "General maintenance tags": [ "cleanup", "complex", "copyedit", "wikify" ], "Potentially unwanted content": [ "external links" ], "Structure, formatting, and lead section": [ "intro-missing", "intro-rewrite", "intro-tooshort", "restructure", "sections", "very long" ], "Fiction-related cleanup": [ "fiction", "in-universe", "plot" ] }, "General content issues": { "Importance and notability": [ "notability" // has subcategories and special-cased code ], "Style of writing": [ "advertisement", "fansite", "jargon", "prose", "redlinks", "tone" ], "Sense (or lack thereof)": [ "confusing" ], "Information and detail": [ "context", "expert-subject", "metricate" ], "Timeliness": [ "update" ], "Neutrality, bias, and factual accuracy": [ "autobiography", "COI", "disputed", "hoax", "globalise", "NPOV", "weasel" ], "Verifiability and sources": [ "BLP sources", "BLP unsourced", "more sources", "no sources", "one source", "original research", "primary sources", "self-published", "unreliable sources" ] }, "Specific content issues": { "Language": [ "complex" ], "Links": [ "dead end", "orphan", "wikify" // this tag is listed twice because it used to focus mainly on links, but now it's a more general cleanup tag ], "Referencing technique": [ "citation style", "link rot", "more footnotes", "no footnotes" ], "Categories": [ "uncat" ] }, "Merging": [ "merge", ], "Informational": [ "in use", "under construction" ] }; // Tags for REDIRECTS start here Twinkle.tag.spellingList = [ { label: '{{R from capitalization}}: redirect from a from a capitalized title', value: 'R from capitalization' }, { label: '{{R with other capitalizations}}: redirect from a title with a different capitalization', value: 'R with other capitalizations' }, { label: '{{R from other name}}: redirect from a title with a different name', value: 'R from other name' }, { label: '{{R from other spelling}}: redirect from a title with a different spelling', value: 'R from other spelling' }, { label: '{{R from plural}}: redirect from a plural title', value: 'R from plural' }, { label: '{{R from related things}}: redirect related title', value: 'R from related things' }, { label: '{{R to section}}: redirect from a title for a "minor topic or title" to a comprehensive-type article section which covers the subject', value: 'R to section' }, { label: '{{R from shortcut}}: redirect to a Wikipedia "shortcut"', value: 'R from shortcut' }, { label: '{{R from title without diacritics}}: redirect to the article title with diacritical marks (accents, umlauts, etc.)', value: 'R from title without diacritics' } ]; // Contains those article tags that *do not* work inside {{multiple issues}}. Twinkle.tag.multipleIssuesExceptions = [ 'cat improve', 'in use', 'merge', 'merge from', 'merge to', 'not English', 'rough translation', 'uncat', 'under construction', ]; Twinkle.tag.callbacks = { main: function( pageobj ) { var params = pageobj.getCallbackParameters(), tagRe, tagText = '', summaryText = 'Added', tags = [], groupableTags = [], i, totalTags var pageText = pageobj.getPageText(); var addTag = function friendlytagAddTag( tagIndex, tagName ) { var currentTag = ""; if( tagName === 'globalize' ) { currentTag += '{{' + params.globalizeSubcategory; } else { currentTag += ( Twinkle.tag.mode === 'redirect' ? '\n' : '' ) + '{{' + tagName; } if( tagName === 'notability' && params.notabilitySubcategory !== 'none' ) { currentTag += '|' + params.notabilitySubcategory; } // prompt for other parameters, based on the tag switch( tagName ) { case 'cleanup': var reason = prompt('"The specific problem is: " \n' + "This information is optional. Just click OK if you don't wish to enter this.", ""); if (reason === null) { Morebits.status.warn("Notice", "{{cleanup}} tag skipped by user"); return true; // continue to next tag } else { currentTag += '|reason=' + reason; } break; case 'complex': var cpreason = prompt('"An editor’s reason for this is:" (e.g. "words like XX") \n' + "Just click OK if you don't wish to enter this. To skip the {{complex}} tag, click Cancel.", ""); if (cpreason === null) { return true; // continue to next tag } else if (cpreason !== "") { currentTag += '|2=' + cpreason; } break; case 'copyedit': var cereason = prompt('"This article may require copy editing for..." (e.g. "consistent spelling") \n' + "Just click OK if you don't wish to enter this. To skip the {{copyedit}} tag, click Cancel.", ""); if (cereason === null) { return true; // continue to next tag } else if (cereason !== "") { currentTag += '|for=' + cereason; } break; case 'expert-subject': var esreason = prompt('"This is because..." \n' + "This information is optional. To skip the {{expert-subject}} tag, click Cancel.", ""); if (esreason === null) { return true; // continue to next tag } else if (esreason !== "") { currentTag += '|1=' + esreason; } break; case 'not English': var langname = prompt('Please enter the name of the language the article is thought to be written in. \n' + "Just click OK if you don't know. To skip the {{not English}} tag, click Cancel.", ""); if (langname === null) { return true; // continue to next tag } else if (langname !== "") { currentTag += '|1=' + langname; } break; case 'rough translation': var roughlang = prompt('Please enter the name of the language the article is thought to have been translated from. \n' + "Just click OK if you don't know. To skip the {{rough translation}} tag, click Cancel.", ""); if (roughlang === null) { return true; // continue to next tag } else if (roughlang !== "") { currentTag += '|1=' + roughlang; } break; case 'wikify': var wreason = prompt('You can optionally enter a more specific reason why the article needs to be wikified: This article needs to be wikified. {{{Your reason here}}} \n' + "Just click OK if you don't wish to enter this. To skip the {{wikify}} tag, click Cancel.", ""); if (wreason === null) { return true; // continue to next tag } else if (wreason !== "") { currentTag += '|reason=' + wreason; } break; case 'merge': case 'merge to': case 'merge from': var param = prompt('Please enter the name of the other article(s) involved in the merge. \n' + "To specify multiple articles, separate them with a vertical pipe (|) character. \n" + "This information is required. Click OK when done, or click Cancel to skip the merge tag.", ""); if (param === null) { return true; // continue to next tag } else if (param !== "") { currentTag += '|' + param; } break; default: break; } currentTag += (Twinkle.tag.mode === 'redirect') ? '}}' : '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n'; tagText += currentTag; if ( tagIndex > 0 ) { if( tagIndex === (totalTags - 1) ) { summaryText += ' and'; } else if ( tagIndex < (totalTags - 1) ) { summaryText += ','; } } summaryText += ' {{[['; summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName)); summaryText += ']]}}'; }; if( Twinkle.tag.mode !== 'redirect' ) { // Check for preexisting tags and separate tags into groupable and non-groupable arrays for( i = 0; i < params.tags.length; i++ ) { tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' ); if( !tagRe.exec( pageText ) ) { if( Twinkle.tag.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) { groupableTags = groupableTags.concat( params.tags[i] ); } else { tags = tags.concat( params.tags[i] ); } } else { Morebits.status.info( 'Info', 'Found {{' + params.tags[i] + '}} on the article already...excluding' ); } } if( params.group && groupableTags.length >= 3 ) { Morebits.status.info( 'Info', 'Grouping supported tags inside {{multiple issues}}' ); groupableTags.sort(); tagText += '{{multiple issues|\n'; totalTags = groupableTags.length; $.each(groupableTags, addTag); summaryText += ' tags (within {{[[Template:multiple issues|multiple issues]]}})'; if( tags.length > 0 ) { summaryText += ', and'; } tagText += '}}\n'; } else { tags = tags.concat( groupableTags ); } } else { // Redirect tagging: Check for pre-existing tags for( i = 0; i < params.tags.length; i++ ) { tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' ); if( !tagRe.exec( pageText ) ) { tags = tags.concat( params.tags[i] ); } else { Morebits.status.info( 'Info', 'Found {{' + params.tags[i] + '}} on the redirect already...excluding' ); } } } tags.sort(); totalTags = tags.length; $.each(tags, addTag); if( Twinkle.tag.mode === 'redirect' ) { pageText += tagText; } else { // smartly insert the new tags after any hatnotes. Regex is a bit more // complicated than it'd need to be, to allow templates as parameters, // and to handle whitespace properly. pageText = pageText.replace(/^\s*(?:((?:\s*\{\{\s*(?:about|correct title|dablink|distinguish|for|other\s?(?:hurricaneuses|people|persons|places|uses(?:of)?)|redirect(?:-acronym)?|see\s?(?:also|wiktionary)|selfref|the)\d*\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\})+(?:\s*\n)?)\s*)?/i, "$1" + tagText); } summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) + ' to ' + Twinkle.tag.mode + Twinkle.getPref('summaryAd'); pageobj.setPageText(pageText); pageobj.setEditSummary(summaryText); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) { pageobj.patrol(); } }, file: function friendlytagCallbacksFile(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var summary = "Adding "; // Add maintenance tags if (params.tags.length) { var tagtext = "", currentTag; $.each(params.tags, function(k, tag) { currentTag += "}}\n"; tagtext += currentTag; summary += "{{" + tag + "}}, "; return true; // continue }); if (!tagtext) { pageobj.getStatusElement().warn("User canceled operation; nothing to do"); return; } text = tagtext + text; } pageobj.setPageText(text); pageobj.setEditSummary(summary.substring(0, summary.length - 2) + Twinkle.getPref('summaryAd')); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchTaggedPages')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markTaggedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if( Twinkle.getFriendlyPref('markTaggedPagesAsPatrolled') ) { pageobj.patrol(); } } }; Twinkle.tag.callback.evaluate = function friendlytagCallbackEvaluate(e) { var form = e.target; var params = {}; switch (Twinkle.tag.mode) { case 'article': params.tags = form.getChecked( 'articleTags' ); params.group = form.group.checked; params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null; break; case 'file': params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null; params.tags = form.getChecked( 'imageTags' ); break; case 'redirect': params.tags = form.getChecked( 'redirectTags' ); break; default: alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode); break; } if( !params.tags.length ) { alert( 'You must select at least one tag!' ); return; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds"; if (Twinkle.tag.mode === 'redirect') { Morebits.wiki.actionCompleted.followRedirect = false; } var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.tag.mode); wikipedia_page.setCallbackParameters(params); switch (Twinkle.tag.mode) { case 'article': /* falls through */ case 'redirect': wikipedia_page.load(Twinkle.tag.callbacks.main); return; case 'file': wikipedia_page.load(Twinkle.tag.callbacks.file); return; default: alert("Twinkle.tag: unknown mode " + Twinkle.tag.mode); break; } }; /* **************************************** *** twinklestub.js: Tag module **************************************** * Mode of invocation: Tab ("Stub") * Active on: Existing articles * Config directives in: FriendlyConfig * Note: customised friendlytag module (for SEWP) */ Twinkle.stub = function friendlytag() { // redirect tagging if( Morebits.wiki.isPageRedirect() ) { Twinkle.stub.mode = 'redirect'; } // file tagging else if( mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById("mw-sharedupload") && document.getElementById("mw-imagepage-section-filehistory") ) { Twinkle.stub.mode = 'file'; } // article/draft article tagging else if( ( mw.config.get('wgNamespaceNumber') === 0 || /^Wikipedia([ _]talk)?\:Requested[ _]pages\//.exec(mw.config.get('wgPageName')) ) && mw.config.get('wgCurRevisionId') ) { Twinkle.stub.mode = 'article'; //twAddPortletLink( Twinkle.stub.callback, "Stub", "friendly-tag", "Add stub tags to article" ); } }; Twinkle.stub.callback = function friendlytagCallback( uid ) { var Window = new Morebits.simpleWindow( 630, (Twinkle.stub.mode === "article") ? 450 : 400 ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Simple Stub project", "Wikipedia:Simple Stub Project" ); Window.addFooterLink( "Stub guideline", "Wikipedia:Stub" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#stub" ); var form = new Morebits.quickForm( Twinkle.stub.callback.evaluate ); switch( Twinkle.stub.mode ) { case 'article': Window.setTitle( "Article stub tagging" ); form.append({ type: 'select', name: 'sortorder', label: 'View this list:', tooltip: 'You can change the default view order in your Twinkle preferences (https://dev.miraheze.org/wiki/Twinkle/Preferences).', event: Twinkle.stub.updateSortOrder, list: [ { type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'cat' }, { type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getFriendlyPref('stubArticleSortOrder') === 'alpha' } ] }); form.append( { type: 'div', id: 'tagWorkArea' } ); } form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); if (Twinkle.stub.mode === "article") { // fake a change event on the sort dropdown, to initialize the tag list var evt = document.createEvent("Event"); evt.initEvent("change", true, true); result.sortorder.dispatchEvent(evt); } }; Twinkle.stub.checkedTags = []; Twinkle.stub.updateSortOrder = function(e) { var sortorder = e.target.value; var $workarea = $(e.target.form).find("div#tagWorkArea"); Twinkle.stub.checkedTags = e.target.form.getChecked("articleTags"); if (!Twinkle.stub.checkedTags) { Twinkle.stub.checkedTags = []; } // function to generate a checkbox, with appropriate subgroup if needed var makeCheckbox = function(tag, description) { var checkbox = { value: tag, label: "{{" + tag + "}}: " + description }; if (Twinkle.stub.checkedTags.indexOf(tag) !== -1) { checkbox.checked = true; } return checkbox; }; // categorical sort order if (sortorder === "cat") { var div = new Morebits.quickForm.element({ type: "div", id: "tagWorkArea" }); // function to iterate through the tags and create a checkbox for each one var doCategoryCheckboxes = function(subdiv, array) { var checkboxes = []; $.each(array, function(k, tag) { var description = Twinkle.stub.article.tags[tag]; checkboxes.push(makeCheckbox(tag, description)); }); subdiv.append({ type: "checkbox", name: "articleTags", list: checkboxes }); }; var i = 0; // go through each category and sub-category and append lists of checkboxes $.each(Twinkle.stub.article.tagCategories, function(title, content) { div.append({ type: "header", id: "tagHeader" + i, label: title }); var subdiv = div.append({ type: "div", id: "tagSubdiv" + i++ }); if ($.isArray(content)) { doCategoryCheckboxes(subdiv, content); } else { $.each(content, function(subtitle, subcontent) { subdiv.append({ type: "div", label: [ Morebits.htmlNode("b", subtitle) ] }); doCategoryCheckboxes(subdiv, subcontent); }); } }); var rendered = div.render(); $workarea.replaceWith(rendered); var $rendered = $(rendered); $rendered.find("h5").css({ 'font-size': '110%', 'margin-top': '1em' }); $rendered.find("div").filter(":has(span.quickformDescription)").css({ 'margin-top': '0.4em' }); } // alphabetical sort order else { var checkboxes = []; $.each(Twinkle.stub.article.tags, function(tag, description) { checkboxes.push(makeCheckbox(tag, description)); }); var tags = new Morebits.quickForm.element({ type: "checkbox", name: "articleTags", list: checkboxes }); $workarea.empty().append(tags.render()); } }; // Tags for ARTICLES start here Twinkle.stub.article = {}; // A list of all article tags, in alphabetical order // To ensure tags appear in the default "categorized" view, add them to the tagCategories hash below. Twinkle.stub.article.tags = { "actor-stub": "for use with articles about actors", "asia-stub": "for use with anything about Asia, except people", "bio-stub": "for use with all people, no matter who or what profession", "biology-stub": "for use with topics related to biology", "chem-stub": "for use with topics related to chemistry", "europe-stub": "for use with anything about Europe, except people", "france-geo-stub": "for use with France geography topics", "food-stub": "for use with anything about food", "geo-stub": "for use with all geographical locations (places, towns, cities, etc)", "history-stub": "for use with history topics", "japan-stub": "for use with anything about Japan, except people", "japan-sports-bio-stub": "for use with Japanese sport biographies", "list-stub": "for use with lists only", "lit-stub": "for use with all literature articles except people", "math-stub": "for use with topics related to mathematics", "med-stub": "for use with topics related to medicine", "military-stub": "for use with military related topics", "movie-stub": "for use with all movie articles except people", "music-stub": "for use with all music articles except people", "north-America-stub": "for use with anything about North America, except people", "performing-arts-stub": "general stub for the performing arts", "physics-stub": "for use with topics related to physics", "politics-stub": "for use with politics related topics", "religion-stub": "for use with religion related topics", "sci-stub": "anything science related (all branches and their tools)", "sport-stub": "general stub for all sports and sports items, not people", "sports-bio-stub": "for use with people who have sport as profession", "stub": "for all stubs that can not fit into any stub we have", "switzerland-stub": "for use with everything about Switzerland, except people", "tech-stub": "for use with technology related articles", "transport-stub": "for use with articles about any moving object (cars, bikes, ships, crafts, planes, rail, buses, trains, etc)", "tv-stub": "for use with all television articles except people", "UK-stub": "for use with anything about the United Kingdom, except people", "US-actor-stub": "for use with United States actor biographies", "US-bio-stub": "for use with United States biographies", "US-geo-stub": "for use with United States geography topics", "US-stub": "for use with anything about the United States, except people and geography", "video-game-stub": "for use with stubs related to video games", "weather-stub": "for articles about weather" }; // A list of tags in order of category // Tags should be in alphabetical order within the categories // Add new categories with discretion - the list is long enough as is! Twinkle.stub.article.tagCategories = { "Stub templates": [ "stub", "list-stub" ], "Countries & Geography": [ "asia-stub", "europe-stub", "france-geo-stub", "geo-stub", "japan-stub", "japan-sports-bio-stub", "north-America-stub", "switzerland-stub", "UK-stub", "US-bio-stub", "US-geo-stub", "US-stub" ], "Miscellaneous": [ "food-stub", "history-stub", "military-stub", "politics-stub", "religion-stub", "transport-stub" ], "People": [ "actor-stub", "bio-stub", "japan-sports-bio-stub", "sports-bio-stub", "US-actor-stub", "US-bio-stub" ], "Science": [ "biology-stub", "chem-stub", "math-stub", "med-stub", "physics-stub", "sci-stub", "weather-stub" ], "Sports": [ "japan-sports-bio-stub", "sport-stub", "sports-bio-stub" ], "Technology": [ "tech-stub", "video-game-stub" ], "Arts": [ "actor-stub", "lit-stub", "movie-stub", "music-stub", "performing-arts-stub", "tv-stub", "US-actor-stub" ] } // Tags for REDIRECTS start here // Contains those article tags that *do not* work inside {{multiple issues}}. Twinkle.stub.multipleIssuesExceptions = [ 'cat improve', 'in use', 'merge', 'merge from', 'merge to', 'not English', 'rough translation', 'uncat', 'under construction', 'update' ]; Twinkle.stub.callbacks = { main: function( pageobj ) { var params = pageobj.getCallbackParameters(), tagRe, tagText = '', summaryText = 'Added', tags = [], groupableTags = [], i, totalTags; // Remove tags that become superfluous with this action var pageText = pageobj.getPageText(); var addTag = function friendlytagAddTag( tagIndex, tagName ) { var currentTag = ""; pageText += '\n\n{{' + tagName + '}}'; if ( tagIndex > 0 ) { if( tagIndex === (totalTags - 1) ) { summaryText += ' and'; } else if ( tagIndex < (totalTags - 1) ) { summaryText += ','; } } summaryText += ' {{[['; summaryText += (tagName.indexOf(":") !== -1 ? tagName : ("Template:" + tagName + "|" + tagName)); summaryText += ']]}}'; }; // Check for preexisting tags and separate tags into groupable and non-groupable arrays for( i = 0; i < params.tags.length; i++ ) { tagRe = new RegExp( '(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im' ); if( !tagRe.exec( pageText ) ) { if( Twinkle.stub.multipleIssuesExceptions.indexOf(params.tags[i]) === -1 ) { groupableTags = groupableTags.concat( params.tags[i] ); } else { tags = tags.concat( params.tags[i] ); } } else { Morebits.status.info( 'Info', 'Found {{' + params.tags[i] + '}} on the article already...excluding' ); } } tags = tags.concat( groupableTags ); tags.sort(); totalTags = tags.length; $.each(tags, addTag); summaryText += ( tags.length > 0 ? ' tag' + ( tags.length > 1 ? 's' : '' ) : '' ) + ' to ' + Twinkle.stub.mode + Twinkle.getPref('summaryAd'); pageobj.setPageText(pageText); pageobj.setEditSummary(summaryText); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchStubbedPages')); pageobj.setMinorEdit(Twinkle.getFriendlyPref('markStubbedPagesAsMinor')); pageobj.setCreateOption('nocreate'); pageobj.save(); if( Twinkle.getFriendlyPref('markStubbedPagesAsPatrolled') ) { pageobj.patrol(); } } }; Twinkle.stub.callback.evaluate = function friendlytagCallbackEvaluate(e) { var form = e.target; var params = {}; switch (Twinkle.stub.mode) { case 'article': params.tags = form.getChecked( 'articleTags' ); params.group = false; params.notabilitySubcategory = form["articleTags.notability"] ? form["articleTags.notability"].value : null; break; case 'file': params.svgSubcategory = form["imageTags.svgCategory"] ? form["imageTags.svgCategory"].value : null; params.tags = form.getChecked( 'imageTags' ); break; case 'redirect': params.tags = form.getChecked( 'redirectTags' ); break; } if( !params.tags.length ) { alert( 'You must select at least one tag!' ); return; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete, reloading article in a few seconds"; if (Twinkle.stub.mode === 'redirect') { Morebits.wiki.actionCompleted.followRedirect = false; } var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging " + Twinkle.stub.mode); wikipedia_page.setCallbackParameters(params); switch (Twinkle.stub.mode) { case 'article': /* falls through */ case 'redirect': wikipedia_page.load(Twinkle.stub.callbacks.main); return; case 'file': wikipedia_page.load(Twinkle.stub.callbacks.file); return; } }; /* **************************************** *** friendlytalkback.js: Talkback module **************************************** * Mode of invocation: Tab ("TB") * Active on: Existing user talk pages * Config directives in: FriendlyConfig */ ;(function(){ Twinkle.talkback = function() { if ( Morebits.getPageAssociatedUser() === false ) { return; } twAddPortletLink( callback, "TB", "friendly-talkback", "Easy talkback" ); }; var callback = function( ) { if( Morebits.getPageAssociatedUser() === mw.config.get("wgUserName") && !confirm("Is it really so bad that you're talking back to yourself?") ){ return; } var Window = new Morebits.simpleWindow( 600, 350 ); Window.setTitle("Talkback"); Window.setScriptName("Twinkle"); Window.addFooterLink( "About {{talkback}}", "Template:Talkback" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#talkback" ); var form = new Morebits.quickForm( callback_evaluate ); form.append({ type: "radio", name: "tbtarget", list: [ { label: "Talkback: my talk page", value: "mytalk", checked: "true" }, { label: "Talkback: other user talk page", value: "usertalk" }, { label: "Talkback: other page", value: "other" }, { label: "Noticeboard notification", value: "notice" }, { label: "\"You've got mail\"", value: "mail" }, { label: "Whisperback", value: "wb" } ], event: callback_change_target }); form.append({ type: "field", label: "Work area", name: "work_area" }); form.append({ type: "submit" }); var result = form.render(); Window.setContent( result ); Window.display(); // We must init the var evt = document.createEvent("Event"); evt.initEvent( "change", true, true ); result.tbtarget[0].dispatchEvent( evt ); }; var prev_page = ""; var prev_section = ""; var prev_message = ""; var callback_change_target = function( e ) { var value = e.target.values; var root = e.target.form; var old_area = Morebits.quickForm.getElements(root, "work_area")[0]; if(root.section) { prev_section = root.section.value; } if(root.message) { prev_message = root.message.value; } if(root.page) { prev_page = root.page.value; } var work_area = new Morebits.quickForm.element({ type: "field", label: "Talkback information", name: "work_area" }); switch( value ) { case "mytalk": /* falls through */ default: work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on your talk page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; case "usertalk": work_area.append({ type:"input", name:"page", label:"User", tooltip:"The username of the user on whose talk page you left a message.", value: prev_page }); work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; case "notice": var noticeboard = work_area.append({ type: "select", name: "noticeboard", label: "Noticeboard:" }); noticeboard.append({ type: "option", label: "WP:AN (Administrators' noticeboard)", value: "an" }); work_area.append({ type:"input", name:"section", label:"Linked thread", tooltip:"The heading of the relevant thread on the noticeboard page.", value: prev_section }); break; case "other": work_area.append({ type:"input", name:"page", label:"Full page name", tooltip:"The full page name where you left the message. For example: 'Wikipedia talk:Twinkle'.", value: prev_page }); work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; case "mail": work_area.append({ type:"input", name:"section", label:"Subject of e-mail (optional)", tooltip:"The subject line of the e-mail you sent." }); break; case "wb": work_area.append({ type:"input", name:"page", label:"User", tooltip:"The username of the user on whose talk page you left a message.", value: prev_page }); work_area.append({ type:"input", name:"section", label:"Linked section (optional)", tooltip:"The section heading on the page where you left a message. Leave empty for no section to be linked.", value: prev_section }); break; } if (value !== "notice") { work_area.append({ type:"textarea", label:"Additional message (optional):", name:"message", tooltip:"An additional message that you would like to leave below the talkback template. Your signature will be added to the end of the message if you leave one." }); } work_area = work_area.render(); root.replaceChild( work_area, old_area ); if (root.message) { root.message.value = prev_message; } }; var callback_evaluate = function( e ) { var tbtarget = e.target.getChecked( "tbtarget" )[0]; var page = null; var section = e.target.section.value; var fullUserTalkPageName = mw.config.get("wgFormattedNamespaces")[ mw.config.get("wgNamespaceIds").user_talk ] + ":" + Morebits.getPageAssociatedUser(); if( tbtarget === "usertalk" || tbtarget === "other" || tbtarget === "wb" ) { page = e.target.page.value; if( tbtarget === "usertalk" ) { if( !page ) { alert("You must specify the username of the user whose talk page you left a message on."); return; } } else { if( !page ) { alert("You must specify the full page name when your message is not on a user talk page."); return; } } } else if (tbtarget === "notice") { page = e.target.noticeboard.value; } var message; if (e.target.message) { message = e.target.message.value; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Morebits.wiki.actionCompleted.redirect = fullUserTalkPageName; Morebits.wiki.actionCompleted.notice = "Talkback complete; reloading talk page in a few seconds"; var talkpage = new Morebits.wiki.page(fullUserTalkPageName, "Adding talkback"); var tbPageName = (tbtarget === "mytalk") ? mw.config.get("wgUserName") : page; var text; if ( tbtarget === "notice" ) { text = "\n\n== " + Twinkle.getFriendlyPref("adminNoticeHeading") + " ==\n"; text += "{{subst:AN-notice|thread=" + section + "|noticeboard=Wikipedia:Administrators' noticeboard}} ~~~~"; talkpage.setEditSummary( "Notice of discussion at [[Wikipedia:Administrators' noticeboard]]" + Twinkle.getPref("summaryAd") ); } else if ( tbtarget === "mail" ) { text = "\n\n==" + Twinkle.getFriendlyPref("mailHeading") + "==\n{{you've got mail|subject="; text += section + "|ts=~~~~~}}"; if( message ) { text += "\n" + message + " ~~~~"; } else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) { text += "\n~~~~"; } talkpage.setEditSummary("Notification: You've got mail" + Twinkle.getPref("summaryAd")); } else if ( tbtarget === "wb" ) { text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{wb|"; text += tbPageName; if( section ) { text += "|" + section; } text += "|ts=~~~~~}}"; if( message ) { text += "\n" + message + " ~~~~"; } else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) { text += "\n~~~~"; } talkpage.setEditSummary("Whisperback" + Twinkle.getPref("summaryAd")); } else { //clean talkback heading: strip section header markers, were erroneously suggested in the documentation text = "\n\n==" + Twinkle.getFriendlyPref("talkbackHeading").replace( /^\s*=+\s*(.*?)\s*=+$\s*/, "$1" ) + "==\n{{tb|"; text += tbPageName; if( section ) { text += "|" + section; } text += "|ts=~~~~~}}"; if( message ) { text += "\n" + message + " ~~~~"; } else if( Twinkle.getFriendlyPref("insertTalkbackSignature") ) { text += "\n~~~~"; } talkpage.setEditSummary("Talkback ([[" + (tbtarget === "other" ? "" : "User talk:") + tbPageName + (section ? ("#" + section) : "") + "]])" + Twinkle.getPref("summaryAd")); } talkpage.setAppendText( text ); talkpage.setCreateOption("recreate"); talkpage.setMinorEdit(Twinkle.getFriendlyPref("markTalkbackAsMinor")); talkpage.setFollowRedirect( true ); talkpage.append(); }; }()); /* **************************************** *** friendlywelcome.js: Welcome module **************************************** * Mode of invocation: Tab ("Wel"), or from links on diff pages * Active on: Existing user talk pages, diff pages * Config directives in: FriendlyConfig */ Twinkle.welcome = function friendlywelcome() { if( Morebits.queryString.exists( 'friendlywelcome' ) ) { if( Morebits.queryString.get( 'friendlywelcome' ) === 'auto' ) { Twinkle.welcome.auto(); } else { Twinkle.welcome.semiauto(); } } else { Twinkle.welcome.normal(); } }; Twinkle.welcome.auto = function() { if( Morebits.queryString.get( 'action' ) !== 'edit' ) { // userpage not empty, aborting auto-welcome return; } Twinkle.welcome.welcomeUser(); }; Twinkle.welcome.semiauto = function() { Twinkle.welcome.callback( mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\"") ); }; Twinkle.welcome.normal = function() { if( Morebits.queryString.exists( 'diff' ) ) { // check whether the contributors' talk pages exist yet var $oList = $("#mw-diff-otitle2").find("span.mw-usertoollinks a.new:contains(talk)").first(); var $nList = $("#mw-diff-ntitle2").find("span.mw-usertoollinks a.new:contains(talk)").first(); if( $oList.length > 0 || $nList.length > 0 ) { var spanTag = function( color, content ) { var span = document.createElement( 'span' ); span.style.color = color; span.appendChild( document.createTextNode( content ) ); return span; }; var welcomeNode = document.createElement('strong'); var welcomeLink = document.createElement('a'); welcomeLink.appendChild( spanTag( 'Black', '[' ) ); welcomeLink.appendChild( spanTag( 'Goldenrod', 'welcome' ) ); welcomeLink.appendChild( spanTag( 'Black', ']' ) ); welcomeNode.appendChild(welcomeLink); if( $oList.length > 0 ) { var oHref = $oList.attr("href"); var oWelcomeNode = welcomeNode.cloneNode( true ); oWelcomeNode.firstChild.setAttribute( 'href', oHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) ); $oList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) ); $oList[0].parentNode.parentNode.appendChild( oWelcomeNode ); } if( $nList.length > 0 ) { var nHref = $nList.attr("href"); var nWelcomeNode = welcomeNode.cloneNode( true ); nWelcomeNode.firstChild.setAttribute( 'href', nHref + '&' + Morebits.queryString.create( { 'friendlywelcome': Twinkle.getFriendlyPref('quickWelcomeMode')==='auto'?'auto':'norm' } ) + '&' + Morebits.queryString.create( { 'vanarticle': mw.config.get( 'wgPageName' ).replace(/_/g, ' ') } ) ); $nList[0].parentNode.parentNode.appendChild( document.createTextNode( ' ' ) ); $nList[0].parentNode.parentNode.appendChild( nWelcomeNode ); } } } if( mw.config.get( 'wgNamespaceNumber' ) === 3 ) { var username = mw.config.get( 'wgTitle' ).split( '/' )[0].replace( /\"/, "\\\""); // only first part before any slashes twAddPortletLink( function(){ Twinkle.welcome.callback(username); }, "Wel", "friendly-welcome", "Welcome user" ); } }; Twinkle.welcome.welcomeUser = function welcomeUser() { Morebits.status.init( document.getElementById('bodyContent') ); var params = { value: Twinkle.getFriendlyPref('quickWelcomeTemplate'), article: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '', mode: 'auto' }; Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.welcome.callbacks.main); }; Twinkle.welcome.callback = function friendlywelcomeCallback( uid ) { if( uid === mw.config.get('wgUserName') && !confirm( 'Are you really sure you want to welcome yourself?...' ) ){ return; } var Window = new Morebits.simpleWindow( 600, 420 ); Window.setTitle( "Welcome user" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#welcome" ); var form = new Morebits.quickForm( Twinkle.welcome.callback.evaluate ); form.append({ type: 'select', name: 'type', label: 'Type of welcome: ', event: Twinkle.welcome.populateWelcomeList, list: [ { type: 'option', value: 'standard', label: 'Standard welcomes', selected: !Morebits.isIPAddress(mw.config.get('wgTitle')) }, { type: 'option', value: 'anonymous', label: 'Problem user welcomes', selected: Morebits.isIPAddress(mw.config.get('wgTitle')) } ] }); form.append( { type: 'div', id: 'welcomeWorkArea' } ); form.append( { type: 'input', name: 'article', label: '* Linked article (if supported by template):', value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ), tooltip: 'An article might be linked from within the welcome if the template supports it. Leave empty for no article to be linked. Templates that support a linked article are marked with an asterisk.' } ); var previewlink = document.createElement( 'a' ); $(previewlink).click(function(){ Twinkle.welcome.callbacks.preview(result); // |result| is defined below }); previewlink.style.cursor = "pointer"; previewlink.textContent = 'Preview'; form.append( { type: 'div', name: 'welcomepreview', label: [ previewlink ] } ); form.append( { type: 'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); // initialize the welcome list var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.type.dispatchEvent( evt ); }; Twinkle.welcome.populateWelcomeList = function(e) { var type = e.target.value; var $workarea = $(e.target.form).find("div#welcomeWorkArea"); var div = new Morebits.quickForm.element({ type: "div", id: "welcomeWorkArea" }); if ((type === "standard" || type === "anonymous") && Twinkle.getFriendlyPref("customWelcomeList").length) { div.append({ type: 'header', label: 'Custom welcome templates' }); div.append({ type: 'radio', name: 'template', list: Twinkle.getFriendlyPref("customWelcomeList"), event: Twinkle.welcome.selectTemplate }); } var appendTemplates = function(list) { div.append({ type: 'radio', name: 'template', list: list.map(function(obj) { var properties = Twinkle.welcome.templates[obj]; var result = (properties ? { value: obj, label: "{{" + obj + "}}: " + properties.description + (properties.linkedArticle ? "\u00A0*" : ""), // U+00A0 NO-BREAK SPACE tooltip: properties.tooltip // may be undefined } : { value: obj, label: "{{" + obj + "}}" }); return result; }), event: Twinkle.welcome.selectTemplate }); }; switch (type) { case "standard": div.append({ type: 'header', label: 'General welcome templates' }); appendTemplates([ "welcome", "welcome2", "welcome-anon", "welcome-anon2", "welcome-en", "welcome-iw", "welcomeg", "welcomeq", "welcome-personal", "welcome-school" ]); break; case "anonymous": div.append({ type: 'header', label: 'Problem user welcome templates' }); appendTemplates([ "firstarticle", "welcomespam", "welcomenpov", "welcomevandal" ]); break; default: div.append({ type: 'div', label: 'Twinkle.welcome.populateWelcomeList: something went wrong' }); break; } var rendered = div.render(); rendered.className = "quickform-scrollbox"; $workarea.replaceWith(rendered); var firstRadio = e.target.form.template[0]; firstRadio.checked = true; Twinkle.welcome.selectTemplate({ target: firstRadio }); }; Twinkle.welcome.selectTemplate = function(e) { var properties = Twinkle.welcome.templates[e.target.values]; e.target.form.article.disabled = (properties ? !properties.linkedArticle : false); }; // A list of welcome templates and their properties and syntax // The four fields that are available are "description", "linkedArticle", "syntax", and "tooltip". // The three magic words that can be used in the "syntax" field are: // - $USERNAME$ - replaced by the welcomer's username, depending on user's preferences // - $ARTICLE$ - replaced by an article name, if "linkedArticle" is true // - $HEADER$ - adds a level 2 header (most templates already include this) Twinkle.welcome.templates = { "welcome": { description: "standard plain text welcome", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome2": { description: "welcome with graphic and orange color sheme", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome2|~~~~|art=$ARTICLE$}}" }, "welcome-anon": { description: "welcome anonymous user and suggest getting a username", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome-anon|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome-anon2": { description: "like welcome-anon, but with table and colors", linkedArticle: true, syntax: "$HEADER$ {{subst:welcome-anon2|$USERNAME$|art=$ARTICLE$}}" }, "welcome-en": { description: "welcome for users from main English Wikipedia", linkedArticle: false, syntax: "$HEADER$ {{subst:welcome-en}} ~~~~" }, "welcome-iw": { description: "welcome users from another Wikipedia", linkedArticle: false, syntax: "$HEADER$ {{subst:welcome-iw}} ~~~~" }, "welcomeg": { description: "welcome with blue background", linkedArticle: true, syntax: "{{subst:welcomeg|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcomeq": { description: "like welcomeg but a bit shorter", linkedArticle: true, syntax: "{{subst:welcomeq|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome-personal": { description: "a more personal welcome with a plate of cookies", linkedArticle: true, syntax: "{{subst:welcome-personal|$USERNAME$|art=$ARTICLE$}} ~~~~" }, "welcome-school": { description: "for welcoming students participating in a class project", linkedArticle: false, syntax: "{{subst:welcome-school}} ~~~~" }, // second group "firstarticle": { description: "welcome with note that created page may get deleted", linkedArticle: true, syntax: "$HEADER$ {{subst:firstarticle|1=$ARTICLE$}} ~~~~" }, "welcomespam": { description: "welcome users which did spam changes", linkedArticle: true, syntax: "$HEADER$ {{subst:welcomespam|art=$ARTICLE$}} ~~~~" }, "welcomenpov": { description: "welcome with warning to make changes that fit the NPOV requirements", linkedArticle: true, syntax: "$HEADER$ {{subst:welcomenpov|$ARTICLE$}} ~~~~" }, "welcomevandal": { description: "welcome user which performed vandalism", linkedArticle: true, syntax: "$HEADER$ {{subst:welcomevandal|$ARTICLE$}} ~~~~" } }; Twinkle.welcome.getTemplateWikitext = function(template, article) { var properties = Twinkle.welcome.templates[template]; if (properties) { return properties.syntax. replace("$USERNAME$", Twinkle.getFriendlyPref("insertUsername") ? mw.config.get("wgUserName") : ""). replace("$ARTICLE$", article ? article : ""). replace(/\$HEADER\$\s*/, "== Welcome ==\n\n"). replace("$EXTRA$", ""); // EXTRA is not implemented yet } else { return "{{subst:" + template + (article ? ("|art=" + article) : "") + "}} ~~~~"; } }; Twinkle.welcome.callbacks = { preview: function(form) { var previewDialog = new Morebits.simpleWindow(750, 400); previewDialog.setTitle("Welcome template preview"); previewDialog.setScriptName("Welcome user"); previewDialog.setModality(true); var previewdiv = document.createElement("div"); previewdiv.style.marginLeft = previewdiv.style.marginRight = "0.5em"; previewdiv.style.fontSize = "small"; previewDialog.setContent(previewdiv); var previewer = new Morebits.wiki.preview(previewdiv); previewer.beginRender(Twinkle.welcome.getTemplateWikitext(form.getChecked("template"), form.article.value)); var submit = document.createElement("input"); submit.setAttribute("type", "submit"); submit.setAttribute("value", "Close"); previewDialog.addContent(submit); previewDialog.display(); $(submit).click(function(e) { previewDialog.close(); }); }, main: function( pageobj ) { var params = pageobj.getCallbackParameters(); var text = pageobj.getPageText(); // abort if mode is auto and form is not empty if( pageobj.exists() && params.mode === 'auto' ) { Morebits.status.info( 'Warning', 'User talk page not empty; aborting automatic welcome' ); Morebits.wiki.actionCompleted.event(); return; } var welcomeText = Twinkle.welcome.getTemplateWikitext(params.value, params.article); if( Twinkle.getFriendlyPref('topWelcomes') ) { text = welcomeText + '\n\n' + text; } else { text += "\n" + welcomeText; } var summaryText = "Welcome to Wikipedia!"; pageobj.setPageText(text); pageobj.setEditSummary(summaryText + Twinkle.getPref('summaryAd')); pageobj.setWatchlist(Twinkle.getFriendlyPref('watchWelcomes')); pageobj.setCreateOption('recreate'); pageobj.save(); } }; Twinkle.welcome.callback.evaluate = function friendlywelcomeCallbackEvaluate(e) { var form = e.target; var params = { value: form.getChecked("template"), article: form.article.value, mode: 'manual' }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Welcoming complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "User talk page modification"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.welcome.callbacks.main); }; /* **************************************** *** twinklearv.js: ARV module **************************************** * Mode of invocation: Tab ("ARV") * Active on: Existing and non-existing user pages, user talk pages, contributions pages * Config directives in: TwinkleConfig */ Twinkle.arv = function twinklearv() { var username = Morebits.getPageAssociatedUser(); if ( username === false ) { return; } var title = Morebits.isIPAddress( username ) ? 'Report IP to administrators' : 'Report user to administrators'; twAddPortletLink( function(){ Twinkle.arv.callback(username); }, "VIP", "tw-arv", title ); }; Twinkle.arv.callback = function ( uid ) { if ( !twinkleUserAuthorized ) { alert("Your account is too new to use Twinkle."); return; } if ( uid === mw.config.get('wgUserName') ) { alert( 'You don\'t want to report yourself, do you?' ); return; } var Window = new Morebits.simpleWindow( 600, 500 ); Window.setTitle( "Vandalism in progress" ); //changed title Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#arv" ); var form = new Morebits.quickForm( Twinkle.arv.callback.evaluate ); var categories = form.append( { type: 'select', name: 'category', label: 'Select report type: ', event: Twinkle.arv.callback.changeCategory } ); categories.append( { type: 'option', label: 'Vandalism (WP:VIP)', value: 'aiv' } ); form.append( { type: 'field', label:'Work area', name: 'work_area' } ); form.append( { type:'submit' } ); form.append( { type: 'hidden', name: 'uid', value: uid } ); var result = form.render(); Window.setContent( result ); Window.display(); // We must init the var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.category.dispatchEvent( evt ); }; Twinkle.arv.callback.changeCategory = function (e) { var value = e.target.value; var root = e.target.form; var old_area = Morebits.quickForm.getElements(root, "work_area")[0]; var work_area = null; switch( value ) { case 'aiv': /* falls through */ default: work_area = new Morebits.quickForm.element( { type: 'field', label: 'Report user for vandalism', name: 'work_area' } ); work_area.append( { type: 'input', name: 'page', label: 'Primary linked page: ', tooltip: 'Leave blank to not link to the page in the report', value: Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '', event: function(e) { var value = e.target.value; var root = e.target.form; if( value === '' ) { root.badid.disabled = root.goodid.disabled = true; } else { root.badid.disabled = false; root.goodid.disabled = root.badid.value === ''; } } } ); work_area.append( { type: 'input', name: 'badid', label: 'Revision ID for target page when vandalised: ', tooltip: 'Leave blank for no diff link', value: Morebits.queryString.exists( 'vanarticlerevid' ) ? Morebits.queryString.get( 'vanarticlerevid' ) : '', disabled: !Morebits.queryString.exists( 'vanarticle' ), event: function(e) { var value = e.target.value; var root = e.target.form; root.goodid.disabled = value === ''; } } ); work_area.append( { type: 'input', name: 'goodid', label: 'Last good revision ID before vandalism of target page: ', tooltip: 'Leave blank for diff link to previous revision', value: Morebits.queryString.exists( 'vanarticlegoodrevid' ) ? Morebits.queryString.get( 'vanarticlegoodrevid' ) : '', disabled: !Morebits.queryString.exists( 'vanarticle' ) || Morebits.queryString.exists( 'vanarticlerevid' ) } ); work_area.append( { type: 'checkbox', name: 'arvtype', list: [ { label: 'Vandalism after final (level 4 or 4im) warning given', value: 'final' }, { label: 'Vandalism after recent (within 1 day) release of block', value: 'postblock' }, { label: 'Evidently a vandalism-only account', value: 'vandalonly', disabled: Morebits.isIPAddress( root.uid.value ) }, { label: 'Account is evidently a spambot or a compromised account', value: 'spambot' }, { label: 'Account is a promotion-only account', value: 'promoonly' } ] } ); work_area.append( { type: 'textarea', name: 'reason', label: 'Comment: ' } ); work_area = work_area.render(); old_area.parentNode.replaceChild( work_area, old_area ); break; } }; Twinkle.arv.callback.evaluate = function(e) { var form = e.target; var reason = ""; var comment = ""; if ( form.reason ) { comment = form.reason.value; } var uid = form.uid.value; var types; switch( form.category.value ) { // Report user for vandalism case 'aiv': /* falls through */ default: types = form.getChecked( 'arvtype' ); if( !types.length && comment === '' ) { alert( 'You must specify some reason' ); return; } types = types.map( function(v) { switch(v) { case 'final': return 'vandalism after final warning'; case 'postblock': return 'vandalism after recent release of block'; case 'spambot': return 'account is evidently a spambot or a compromised account'; case 'vandalonly': return 'actions evidently indicate a vandalism-only account'; case 'promoonly': return 'account is being used only for promotional purposes'; default: return 'unknown reason'; } } ).join( '; ' ); if ( form.page.value !== '' ) { // add a leading : on linked page namespace to prevent transclusion reason = 'On [[' + form.page.value.replace( /^(Image|Category|File):/i, ':$1:' ) + ']]'; if ( form.badid.value !== '' ) { var query = { 'title': form.page.value, 'diff': form.badid.value, 'oldid': form.goodid.value }; reason += ' ({{diff|' + form.page.value + '|' + form.badid.value + '|' + form.goodid.value + '|diff}})'; } reason += ':'; } if ( types ) { reason += " " + types; } if (comment !== "" ) { reason += (reason === "" ? "" : ". ") + comment; } reason += ". ~~~~"; reason = reason.replace(/\r?\n/g, "\n*:"); // indent newlines Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = "Wikipedia:Vandalism in progress"; Morebits.wiki.actionCompleted.notice = "Reporting complete"; var aivPage = new Morebits.wiki.page( 'Wikipedia:Vandalism in progress', 'Processing VIP request' ); aivPage.setPageSection( 3 ); aivPage.setFollowRedirect( true ); aivPage.load( function() { var text = aivPage.getPageText(); // check if user has already been reported if (new RegExp( "\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*" + RegExp.escape( uid, true ) + "\\s*\\}\\}" ).test(text)) { aivPage.getStatusElement().info( 'Report already present, will not add a new one' ); return; } aivPage.getStatusElement().status( 'Adding new report...' ); aivPage.setEditSummary( 'Reporting [[Special:Contributions/' + uid + '|' + uid + ']].' + Twinkle.getPref('summaryAd') ); aivPage.setAppendText( '\n*{{' + ( Morebits.isIPAddress( uid ) ? 'IPvandal' : 'vandal' ) + '|' + (/\=/.test( uid ) ? '1=' : '' ) + uid + '}} &ndash; ' + reason ); aivPage.append(); } ); break; } }; /* **************************************** *** twinklebatchdelete.js: Batch delete module (sysops only) **************************************** * Mode of invocation: Tab ("D-batch") * Active on: Existing and non-existing non-articles, and Special:PrefixIndex * Config directives in: TwinkleConfig */ Twinkle.batchdelete = function twinklebatchdelete() { if( Morebits.userIsInGroup( 'sysop' ) && (mw.config.get( 'wgNamespaceNumber' ) > 0 || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) { twAddPortletLink( Twinkle.batchdelete.callback, "D-batch", "tw-batch", "Delete pages found in this category/on this page" ); } }; Twinkle.batchdelete.unlinkCache = {}; Twinkle.batchdelete.callback = function twinklebatchdeleteCallback() { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setTitle( "Batch deletion" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#batchdelete" ); var form = new Morebits.quickForm( Twinkle.batchdelete.callback.evaluate ); form.append( { type: 'checkbox', list: [ { label: 'Delete pages', name: 'delete_page', value: 'delete', checked: true }, { label: 'Remove backlinks to the page', name: 'unlink_page', value: 'unlink', checked: false }, { label: 'Delete redirects to deleted pages', name: 'delete_redirects', value: 'delete_redirects', checked: true } ] } ); form.append( { type: 'textarea', name: 'reason', label: 'Reason: ' } ); var query; if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // Category: query = { 'action': 'query', 'generator': 'categorymembers', 'gcmtitle': mw.config.get( 'wgPageName' ), 'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': [ 'categories', 'revisions' ], 'rvprop': [ 'size' ] }; } else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) { var gapnamespace, gapprefix; if(Morebits.queryString.exists( 'from' ) ) { gapnamespace = Morebits.queryString.get( 'namespace' ); gapprefix = Morebits.string.toUpperCaseFirstChar( Morebits.queryString.get( 'from' ) ); } else { var pathSplit = location.pathname.split('/'); if (pathSplit.length < 3 || pathSplit[2] !== "Special:PrefixIndex") { return; } var titleSplit = pathSplit[3].split(':'); gapnamespace = mw.config.get("wgNamespaceIds")[titleSplit[0].toLowerCase()]; if ( titleSplit.length < 2 || typeof gapnamespace === 'undefined' ) { gapnamespace = 0; // article namespace gapprefix = pathSplit.splice(3).join('/'); } else { pathSplit = pathSplit.splice(4); pathSplit.splice(0,0,titleSplit.splice(1).join(':')); gapprefix = pathSplit.join('/'); } } query = { 'action': 'query', 'generator': 'allpages', 'gapnamespace': gapnamespace , 'gapprefix': gapprefix, 'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop' : ['categories', 'revisions' ], 'rvprop': [ 'size' ] }; } else { query = { 'action': 'query', 'generator': 'links', 'titles': mw.config.get( 'wgPageName' ), 'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': [ 'categories', 'revisions' ], 'rvprop': [ 'size' ] }; } var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//page[@ns != "6" and not(@missing)]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); // 6 = File: namespace var list = []; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var object = snapshot.snapshotItem(i); var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue; var size = xmlDoc.evaluate( 'revisions/rev/@size', object, null, XPathResult.NUMBER_TYPE, null ).numberValue; var disputed = xmlDoc.evaluate( 'boolean(categories/cl[@title="Category:Contested candidates for speedy deletion"])', object, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue; list.push( {label:page + ' (' + size + ' bytes)' + ( disputed ? ' (DISPUTED CSD)' : '' ), value:page, checked:!disputed }); } self.params.form.append( { type: 'checkbox', name: 'pages', list: list } ); self.params.form.append( { type:'submit' } ); var result = self.params.form.render(); self.params.Window.setContent( result ); } ); wikipedia_api.params = { form:form, Window:Window }; wikipedia_api.post(); var root = document.createElement( 'div' ); Morebits.status.init( root ); Window.setContent( root ); Window.display(); }; Twinkle.batchdelete.currentDeleteCounter = 0; Twinkle.batchdelete.currentUnlinkCounter = 0; Twinkle.batchdelete.currentdeletor = 0; Twinkle.batchdelete.callback.evaluate = function twinklebatchdeleteCallbackEvaluate(event) { Morebits.wiki.actionCompleted.notice = 'Status'; Morebits.wiki.actionCompleted.postfix = 'batch deletion is now complete'; mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var pages = event.target.getChecked( 'pages' ); var reason = event.target.reason.value; var delete_page = event.target.delete_page.checked; var unlink_page = event.target.unlink_page.checked; var delete_redirects = event.target.delete_redirects.checked; if( ! reason ) { return; } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( event.target ); if( !pages ) { Morebits.status.error( 'Error', 'nothing to delete, aborting' ); return; } function toCall( work ) { if( work.length === 0 && Twinkle.batchdelete.currentDeleteCounter <= 0 && Twinkle.batchdelete.currentUnlinkCounter <= 0 ) { window.clearInterval( Twinkle.batchdelete.currentdeletor ); Morebits.wiki.removeCheckpoint(); return; } else if( work.length !== 0 && ( Twinkle.batchdelete.currentDeleteCounter <= Twinkle.getPref('batchDeleteMinCutOff') || Twinkle.batchdelete.currentUnlinkCounter <= Twinkle.getPref('batchDeleteMinCutOff') ) ) { Twinkle.batchdelete.unlinkCache = []; // Clear the cache var pages = work.shift(); Twinkle.batchdelete.currentDeleteCounter += pages.length; Twinkle.batchdelete.currentUnlinkCounter += pages.length; for( var i = 0; i < pages.length; ++i ) { var page = pages[i]; var query = { 'action': 'query', 'titles': page }; var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchdelete.callbacks.main ); wikipedia_api.params = { page:page, reason:reason, unlink_page:unlink_page, delete_page:delete_page, delete_redirects:delete_redirects }; wikipedia_api.post(); } } } var work = Morebits.array.chunk( pages, Twinkle.getPref('batchdeleteChunks') ); Morebits.wiki.addCheckpoint(); Twinkle.batchdelete.currentdeletor = window.setInterval( toCall, 1000, work ); }; Twinkle.batchdelete.callbacks = { main: function( self ) { var xmlDoc = self.responseXML; var normal = xmlDoc.evaluate( '//normalized/n/@to', xmlDoc, null, XPathResult.STRING_TYPE, null ).stringValue; if( normal ) { self.params.page = normal; } var exists = xmlDoc.evaluate( 'boolean(//pages/page[not(@missing)])', xmlDoc, null, XPathResult.BOOLEAN_TYPE, null ).booleanValue; if( ! exists ) { self.statelem.error( "It seems that the page doesn't exist, perhaps it has already been deleted" ); return; } var query, wikipedia_api; if( self.params.unlink_page ) { query = { 'action': 'query', 'list': 'backlinks', 'blfilterredir': 'nonredirects', 'blnamespace': [0, 100], // main space and portal space only 'bltitle': self.params.page, 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops }; wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.batchdelete.callbacks.unlinkBacklinksMain ); wikipedia_api.params = self.params; wikipedia_api.post(); } else { --Twinkle.batchdelete.currentUnlinkCounter; } if( self.params.delete_page ) { if (self.params.delete_redirects) { query = { 'action': 'query', 'list': 'backlinks', 'blfilterredir': 'redirects', 'bltitle': self.params.page, 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 // 500 is max for normal users, 5000 for bots and sysops }; wikipedia_api = new Morebits.wiki.api( 'Grabbing redirects', query, Twinkle.batchdelete.callbacks.deleteRedirectsMain ); wikipedia_api.params = self.params; wikipedia_api.post(); } var wikipedia_page = new Morebits.wiki.page( self.params.page, 'Deleting page ' + self.params.page ); wikipedia_page.setEditSummary(self.params.reason + Twinkle.getPref('deletionSummaryAd')); wikipedia_page.deletePage(function( apiobj ) { --Twinkle.batchdelete.currentDeleteCounter; var link = document.createElement( 'a' ); link.setAttribute( 'href', mw.util.getUrl(self.params.page) ); link.setAttribute( 'title', self.params.page ); link.appendChild( document.createTextNode( self.params.page ) ); apiobj.statelem.info( [ 'completed (' , link , ')' ] ); } ); } else { --Twinkle.batchdelete.currentDeleteCounter; } }, deleteRedirectsMain: function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); var total = snapshot.snapshotLength; if( snapshot.snapshotLength === 0 ) { return; } var statusIndicator = new Morebits.status('Deleting redirects for ' + self.params.page, '0%'); var onsuccess = function( self ) { var obj = self.params.obj; var total = self.params.total; var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%'; obj.update( now ); self.statelem.unlink(); if( self.params.current >= total ) { obj.info( now + ' (completed)' ); Morebits.wiki.removeCheckpoint(); } }; Morebits.wiki.addCheckpoint(); if( snapshot.snapshotLength === 0 ) { statusIndicator.info( '100% (completed)' ); Morebits.wiki.removeCheckpoint(); return; } var params = $.extend({}, self.params); params.current = 0; params.total = total; params.obj = statusIndicator; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var title = snapshot.snapshotItem(i).value; var wikipedia_page = new Morebits.wiki.page( title, "Deleting " + title ); wikipedia_page.setEditSummary('[[WP:QD#G8|G8]]: Redirect to deleted page "' + self.params.page + '"' + Twinkle.getPref('deletionSummaryAd')); wikipedia_page.setCallbackParameters(params); wikipedia_page.deletePage(onsuccess); } }, unlinkBacklinksMain: function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//backlinks/bl/@title', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); if( snapshot.snapshotLength === 0 ) { --Twinkle.batchdelete.currentUnlinkCounter; return; } var statusIndicator = new Morebits.status('Unlinking backlinks to ' + self.params.page, '0%'); var total = snapshot.snapshotLength * 2; var onsuccess = function( self ) { var obj = self.params.obj; var total = self.params.total; var now = parseInt( 100 * ++(self.params.current)/total, 10 ) + '%'; obj.update( now ); self.statelem.unlink(); if( self.params.current >= total ) { obj.info( now + ' (completed)' ); --Twinkle.batchdelete.currentUnlinkCounter; Morebits.wiki.removeCheckpoint(); } }; Morebits.wiki.addCheckpoint(); if( snapshot.snapshotLength === 0 ) { statusIndicator.info( '100% (completed)' ); --Twinkle.batchdelete.currentUnlinkCounter; Morebits.wiki.removeCheckpoint(); return; } self.params.total = total; self.params.obj = statusIndicator; self.params.current = 0; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var title = snapshot.snapshotItem(i).value; var wikipedia_page = new Morebits.wiki.page( title, "Unlinking on " + title ); var params = $.extend( {}, self.params ); params.title = title; params.onsuccess = onsuccess; wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.batchdelete.callbacks.unlinkBacklinks); } }, unlinkBacklinks: function( pageobj ) { var params = pageobj.getCallbackParameters(); if( ! pageobj.exists() ) { // we probably just deleted it, as a recursive backlink params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } ); Morebits.wiki.actionCompleted(); return; } var text; if( params.title in Twinkle.batchdelete.unlinkCache ) { text = Twinkle.batchdelete.unlinkCache[ params.title ]; } else { text = pageobj.getPageText(); } var old_text = text; var wikiPage = new Morebits.wikitext.page( text ); wikiPage.removeLink( params.page ); text = wikiPage.getText(); Twinkle.batchdelete.unlinkCache[ params.title ] = text; if( text === old_text ) { // Nothing to do, return params.onsuccess( { params: params, statelem: pageobj.getStatusElement() } ); Morebits.wiki.actionCompleted(); return; } pageobj.setEditSummary('Removing link(s) to deleted page ' + self.params.page + Twinkle.getPref('deletionSummaryAd')); pageobj.setPageText(text); pageobj.setCreateOption('nocreate'); pageobj.save(params.onsuccess); } }; /* **************************************** *** twinklebatchprotect.js: Batch protect module (sysops only) **************************************** * Mode of invocation: Tab ("P-batch") * Active on: Existing project pages and user pages; existing and * non-existing categories; Special:PrefixIndex * Config directives in: TwinkleConfig */ Twinkle.batchprotect = function twinklebatchprotect() { if( Morebits.userIsInGroup( 'sysop' ) && ((mw.config.get( 'wgArticleId' ) > 0 && (mw.config.get( 'wgNamespaceNumber' ) === 2 || mw.config.get( 'wgNamespaceNumber' ) === 4)) || mw.config.get( 'wgNamespaceNumber' ) === 14 || mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex') ) { twAddPortletLink( Twinkle.batchprotect.callback, "P-batch", "tw-pbatch", "Protect pages linked from this page" ); } }; Twinkle.batchprotect.unlinkCache = {}; Twinkle.batchprotect.callback = function twinklebatchprotectCallback() { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setTitle( "Batch protection" ); Window.setScriptName( "Twinkle" ); //Window.addFooterLink( "Protection templates", "Template:Protection templates" ); Window.addFooterLink( "Protection policy", "WP:PROT" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#protect" ); var form = new Morebits.quickForm( Twinkle.batchprotect.callback.evaluate ); form.append({ type: 'checkbox', name: 'editmodify', event: Twinkle.protect.formevents.editmodify, list: [ { label: 'Modify edit protection', value: 'editmodify', tooltip: 'Only for existing pages.', checked: true } ] }); var editlevel = form.append({ type: 'select', name: 'editlevel', label: 'Edit protection:', event: Twinkle.protect.formevents.editlevel }); editlevel.append({ type: 'option', label: 'All', value: 'all' }); editlevel.append({ type: 'option', label: 'Autoconfirmed', value: 'autoconfirmed' }); editlevel.append({ type: 'option', label: 'Sysop', value: 'sysop', selected: true }); form.append({ type: 'select', name: 'editexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, list: [ { label: '1 hour', value: '1 hour' }, { label: '2 hours', value: '2 hours' }, { label: '3 hours', value: '3 hours' }, { label: '6 hours', value: '6 hours' }, { label: '12 hours', value: '12 hours' }, { label: '1 day', value: '1 day' }, { label: '2 days', selected: true, value: '2 days' }, { label: '3 days', value: '3 days' }, { label: '4 days', value: '4 days' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '2 months', value: '2 months' }, { label: '3 months', value: '3 months' }, { label: '1 year', value: '1 year' }, { label: 'indefinite', value:'indefinite' }, { label: 'Custom...', value: 'custom' } ] }); form.append({ type: 'checkbox', name: 'movemodify', event: Twinkle.protect.formevents.movemodify, list: [ { label: 'Modify move protection', value: 'movemodify', tooltip: 'Only for existing pages.', checked: true } ] }); var movelevel = form.append({ type: 'select', name: 'movelevel', label: 'Move protection:', event: Twinkle.protect.formevents.movelevel }); movelevel.append({ type: 'option', label: 'All', value: 'all' }); movelevel.append({ type: 'option', label: 'Autoconfirmed', value: 'autoconfirmed' }); movelevel.append({ type: 'option', label: 'Sysop', value: 'sysop', selected: true }); form.append({ type: 'select', name: 'moveexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, list: [ { label: '1 hour', value: '1 hour' }, { label: '2 hours', value: '2 hours' }, { label: '3 hours', value: '3 hours' }, { label: '6 hours', value: '6 hours' }, { label: '12 hours', value: '12 hours' }, { label: '1 day', value: '1 day' }, { label: '2 days', selected: true, value: '2 days' }, { label: '3 days', value: '3 days' }, { label: '4 days', value: '4 days' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '2 months', value: '2 months' }, { label: '3 months', value: '3 months' }, { label: '1 year', value: '1 year' }, { label: 'indefinite', value:'indefinite' }, { label: 'Custom...', value: 'custom' } ] }); form.append({ type: 'checkbox', name: 'createmodify', event: function twinklebatchprotectFormCreatemodifyEvent(e) { e.target.form.createlevel.disabled = !e.target.checked; e.target.form.createexpiry.disabled = !e.target.checked || (e.target.form.createlevel.value === 'all'); e.target.form.createlevel.style.color = e.target.form.createexpiry.style.color = (e.target.checked ? "" : "transparent"); }, list: [ { label: 'Modify create protection', value: 'createmodify', tooltip: 'Only for pages that do not exist.', checked: true } ] }); var createlevel = form.append({ type: 'select', name: 'createlevel', label: 'Create protection:', event: Twinkle.protect.formevents.createlevel }); createlevel.append({ type: 'option', label: 'All', value: 'all' }); createlevel.append({ type: 'option', label: 'Autoconfirmed', value: 'autoconfirmed' }); createlevel.append({ type: 'option', label: 'Sysop', value: 'sysop', selected: true }); form.append({ type: 'select', name: 'createexpiry', label: 'Expires:', event: function(e) { if (e.target.value === 'custom') { Twinkle.protect.doCustomExpiry(e.target); } }, list: [ { label: '1 hour', value: '1 hour' }, { label: '2 hours', value: '2 hours' }, { label: '3 hours', value: '3 hours' }, { label: '6 hours', value: '6 hours' }, { label: '12 hours', value: '12 hours' }, { label: '1 day', value: '1 day' }, { label: '2 days', value: '2 days' }, { label: '3 days', value: '3 days' }, { label: '4 days', value: '4 days' }, { label: '1 week', value: '1 week' }, { label: '2 weeks', value: '2 weeks' }, { label: '1 month', value: '1 month' }, { label: '2 months', value: '2 months' }, { label: '3 months', value: '3 months' }, { label: '1 year', value: '1 year' }, { label: 'indefinite', selected: true, value: 'indefinite' }, { label: 'Custom...', value: 'custom' } ] }); form.append( { type: 'textarea', name: 'reason', label: 'Reason (for protection log): ' } ); var query; if( mw.config.get( 'wgNamespaceNumber' ) === 14 ) { // categories query = { 'action': 'query', 'generator': 'categorymembers', 'gcmtitle': mw.config.get( 'wgPageName' ), 'gcmlimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': 'revisions', 'rvprop': 'size' }; } else if( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Prefixindex' ) { query = { 'action': 'query', 'generator': 'allpages', 'gapnamespace': Morebits.queryString.exists('namespace') ? Morebits.queryString.get( 'namespace' ) : document.getElementById('namespace').value, 'gapprefix': Morebits.queryString.exists('from') ? Morebits.string.toUpperCaseFirstChar(Morebits.queryString.get( 'from' ).replace('+', ' ')) : Morebits.string.toUpperCaseFirstChar(document.getElementById('nsfrom').value), 'gaplimit' : Twinkle.getPref('batchMax'), // the max for sysops 'prop': 'revisions', 'rvprop': 'size' }; } else { query = { 'action': 'query', 'gpllimit' : Twinkle.getPref('batchMax'), // the max for sysops 'generator': 'links', 'titles': mw.config.get( 'wgPageName' ), 'prop': 'revisions', 'rvprop': 'size' }; } var statusdiv = document.createElement("div"); statusdiv.style.padding = '15px'; // just so it doesn't look broken Window.setContent(statusdiv); Morebits.status.init(statusdiv); Window.display(); var statelem = new Morebits.status("Grabbing list of pages"); var wikipedia_api = new Morebits.wiki.api( 'loading...', query, function(apiobj) { var xml = apiobj.responseXML; var $pages = $(xml).find('page'); var list = []; $pages.each(function(index, page) { var $page = $(page); var title = $page.attr('title'); var isRedir = $page.attr('redirect') === ""; // XXX ?? var missing = $page.attr('missing') === ""; // XXX ?? var size = $page.find('rev').attr('size'); var metadata = []; if (missing) { metadata.push("page does not exist"); } else { if (isRedir) { metadata.push("redirect"); } metadata.push(size + " bytes"); } list.push( { label: title + (metadata.length ? (' (' + metadata.join('; ') + ')') : '' ), value: title, checked: true }); }); form.append({ type: 'header', label: 'Pages to protect' }); form.append( { type: 'checkbox', name: 'pages', list: list } ); form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); }, statelem ); wikipedia_api.post(); }; Twinkle.batchprotect.currentProtectCounter = 0; Twinkle.batchprotect.currentprotector = 0; Twinkle.batchprotect.callback.evaluate = function twinklebatchprotectCallbackEvaluate(event) { var pages = event.target.getChecked( 'pages' ); var reason = event.target.reason.value; var editmodify = event.target.editmodify.checked; var editlevel = event.target.editlevel.value; var editexpiry = event.target.editexpiry.value; var movemodify = event.target.movemodify.checked; var movelevel = event.target.movelevel.value; var moveexpiry = event.target.moveexpiry.value; var createmodify = event.target.createmodify.checked; var createlevel = event.target.createlevel.value; var createexpiry = event.target.createexpiry.value; if( ! reason ) { alert("You've got to give a reason, you rouge admin!"); return; } Morebits.simpleWindow.setButtonsEnabled(false); Morebits.status.init( event.target ); if( !pages ) { Morebits.status.error( 'Error', 'Nothing to protect, aborting' ); return; } var toCall = function twinklebatchprotectToCall( work ) { if( work.length === 0 && Twinkle.batchprotect.currentProtectCounter <= 0 ) { Morebits.status.info( 'work done' ); window.clearInterval( Twinkle.batchprotect.currentprotector ); Twinkle.batchprotect.currentprotector = Twinkle.batchprotect.currentProtectCounter = 0; Morebits.wiki.removeCheckpoint(); return; } else if( work.length !== 0 && Twinkle.batchprotect.currentProtectCounter <= Twinkle.getPref('batchProtectMinCutOff') ) { var pages = work.shift(); Twinkle.batchprotect.currentProtectCounter += pages.length; for( var i = 0; i < pages.length; ++i ) { var page = pages[i]; var query = { 'action': 'query', 'titles': page }; var wikipedia_api = new Morebits.wiki.api( 'Checking if page ' + page + ' exists', query, Twinkle.batchprotect.callbacks.main ); wikipedia_api.params = { page: page, reason: reason, editmodify: editmodify, editlevel: editlevel, editexpiry: editexpiry, movemodify: movemodify, movelevel: movelevel, moveexpiry: moveexpiry, createmodify: createmodify, createlevel: createlevel, createexpiry: createexpiry }; wikipedia_api.post(); } } }; var work = Morebits.array.chunk( pages, Twinkle.getPref('batchProtectChunks') ); Morebits.wiki.addCheckpoint(); Twinkle.batchprotect.currentprotector = window.setInterval( toCall, 1000, work ); }; Twinkle.batchprotect.callbacks = { main: function( apiobj ) { var xml = apiobj.responseXML; var normal = $(xml).find('normalized n').attr('to'); if( normal ) { apiobj.params.page = normal; } var exists = ($(xml).find('page').attr('missing') !== ""); var page = new Morebits.wiki.page(apiobj.params.page, "Protecting " + apiobj.params.page); var takenAction = false; if (exists && apiobj.params.editmodify) { page.setEditProtection(apiobj.params.editlevel, apiobj.params.editexpiry); takenAction = true; } if (exists && apiobj.params.movemodify) { page.setMoveProtection(apiobj.params.movelevel, apiobj.params.moveexpiry); takenAction = true; } if (!exists && apiobj.params.createmodify) { page.setCreateProtection(apiobj.params.createlevel, apiobj.params.createexpiry); takenAction = true; } if (!takenAction) { Morebits.status.warn("Protecting " + apiobj.params.page, "page " + (exists ? "exists" : "does not exist") + "; nothing to do, skipping"); return; } page.setEditSummary(apiobj.params.reason); page.protect(function(pageobj) { --Twinkle.batchprotect.currentProtectCounter; var link = document.createElement( 'a' ); link.setAttribute( 'href', mw.util.getUrl( apiobj.params.page ) ); link.appendChild( document.createTextNode( apiobj.params.page ) ); pageobj.getStatusElement().info( [ 'completed (' , link , ')' ] ); } ); } }; /* **************************************** *** twinklebatchundelete.js: Batch undelete module **************************************** * Mode of invocation: Tab ("Und-batch") * Active on: Existing and non-existing user pages (??? why?) * Config directives in: TwinkleConfig */ // XXX TODO this module needs to be overhauled to use Morebits.wiki.page Twinkle.batchundelete = function twinklebatchundelete() { if( mw.config.get("wgNamespaceNumber") !== mw.config.get("wgNamespaceIds").user ) { return; } if( Morebits.userIsInGroup( 'sysop' ) ) { twAddPortletLink( Twinkle.batchundelete.callback, "Und-batch", "tw-batch-undel", "Undelete 'em all" ); } }; Twinkle.batchundelete.callback = function twinklebatchundeleteCallback() { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setScriptName("Twinkle"); Window.setTitle("Batch undelete") var form = new Morebits.quickForm( Twinkle.batchundelete.callback.evaluate ); form.append( { type: 'textarea', name: 'reason', label: 'Reason: ' } ); var query = { 'action': 'query', 'generator': 'links', 'titles': mw.config.get("wgPageName"), 'gpllimit' : Twinkle.getPref('batchMax') // the max for sysops }; var wikipedia_api = new Morebits.wiki.api( 'Grabbing pages', query, function( self ) { var xmlDoc = self.responseXML; var snapshot = xmlDoc.evaluate('//page[@missing]', xmlDoc, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null ); var list = []; for ( var i = 0; i < snapshot.snapshotLength; ++i ) { var object = snapshot.snapshotItem(i); var page = xmlDoc.evaluate( '@title', object, null, XPathResult.STRING_TYPE, null ).stringValue; list.push( {label:page, value:page, checked: true }); } self.params.form.append( { type: 'checkbox', name: 'pages', list: list } ); self.params.form.append( { type:'submit' } ); var result = self.params.form.render(); self.params.Window.setContent( result ); } ); wikipedia_api.params = { form:form, Window:Window }; wikipedia_api.post(); var root = document.createElement( 'div' ); Morebits.status.init( root ); Window.setContent( root ); Window.display(); }; Twinkle.batchundelete.currentUndeleteCounter = 0; Twinkle.batchundelete.currentundeletor = 0; Twinkle.batchundelete.callback.evaluate = function( event ) { Morebits.wiki.actionCompleted.notice = 'Status'; Morebits.wiki.actionCompleted.postfix = 'batch undeletion is now completed'; var pages = event.target.getChecked( 'pages' ); var reason = event.target.reason.value; if( ! reason ) { alert("You need to give a reason, you cabal crony!"); return; } Morebits.simpleWindow.setButtonsEnabled(false); Morebits.status.init( event.target ); if( !pages ) { Morebits.status.error( 'Error', 'nothing to undelete, aborting' ); return; } var work = Morebits.array.chunk( pages, Twinkle.getPref('batchUndeleteChunks') ); Morebits.wiki.addCheckpoint(); Twinkle.batchundelete.currentundeletor = window.setInterval( Twinkle.batchundelete.callbacks.main, 1000, work, reason ); }; Twinkle.batchundelete.callbacks = { main: function( work, reason ) { if( work.length === 0 && Twinkle.batchundelete.currentUndeleteCounter <= 0 ) { Morebits.status.info( 'work done' ); window.clearInterval( Twinkle.batchundelete.currentundeletor ); Morebits.wiki.removeCheckpoint(); return; } else if( work.length !== 0 && Twinkle.batchundelete.currentUndeleteCounter <= Twinkle.getPref('batchUndeleteMinCutOff') ) { var pages = work.shift(); Twinkle.batchundelete.currentUndeleteCounter += pages.length; for( var i = 0; i < pages.length; ++i ) { var title = pages[i]; var query = { 'token': mw.user.tokens.get().editToken, 'title': title, 'action': 'undelete', 'reason': reason + Twinkle.getPref('deletionSummaryAd') }; var wikipedia_api = new Morebits.wiki.api( "Undeleting " + title, query, function( self ) { --Twinkle.batchundelete.currentUndeleteCounter; var link = document.createElement( 'a' ); link.setAttribute( 'href', mw.util.getUrl(self.itsTitle) ); link.setAttribute( 'title', self.itsTitle ); link.appendChild( document.createTextNode(self.itsTitle) ); self.statelem.info( ['completed (',link,')'] ); }); wikipedia_api.itsTitle = title; wikipedia_api.post(); } } } }; /* **************************************** *** twinkleconfig.js: Preferences module **************************************** * Mode of invocation: Adds configuration form to Wikipedia:Twinkle/Preferences and user subpages named "/Twinkle preferences", and adds ad box to the top of user subpages belonging to the currently logged-in user which end in '.js' * Active on: What I just said. Yeah. * Config directives in: TwinkleConfig I, [[User:This, that and the other]], originally wrote this. If the code is misbehaving, or you have any questions, don't hesitate to ask me. (This doesn't at all imply [[WP:OWN]]ership - it's just meant to point you in the right direction.) -- TTO */ Twinkle.config = {}; Twinkle.config.commonEnums = { watchlist: { yes: "Add to watchlist", no: "Don't add to watchlist", "default": "Follow your site preferences" }, talkPageMode: { window: "In a window, replacing other user talks", tab: "In a new tab", blank: "In a totally new window" } }; Twinkle.config.commonSets = { csdCriteria: { db: "Custom rationale" }, csdCriteriaDisplayOrder: [ "db" ], csdCriteriaNotificationDisplayOrder: [ "db" ], csdAndDICriteria: { db: "Custom rationale" }, csdAndDICriteriaDisplayOrder: [ "db" ], namespacesNoSpecial: { "0": "Article", "1": "Talk (article)", "2": "User", "3": "User talk", "4": "Wikipedia", "5": "Wikipedia talk", "6": "File", "7": "File talk", "8": "MediaWiki", "9": "MediaWiki talk", "10": "Template", "11": "Template talk", "12": "Help", "13": "Help talk", "14": "Category", "15": "Category talk" } }; /** * Section entry format: * * { * title: <human-readable section title>, * adminOnly: <true for admin-only sections>, * hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>, * inFriendlyConfig: <true for preferences located under FriendlyConfig rather than TwinkleConfig>, * preferences: [ * { * name: <TwinkleConfig property name>, * label: <human-readable short description - used as a form label>, * helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.> * adminOnly: <true for admin-only preferences>, * type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }), * enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>, * setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>, * setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>, * customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>, * customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor> * }, * . . . * ] * }, * . . . * */ Twinkle.config.sections = [ { title: "General", preferences: [ // TwinkleConfig.summaryAd (string) // Text to be appended to the edit summary of edits made using Twinkle { name: "summaryAd", label: "\"Ad\" to be appended to Twinkle's edit summaries", helptip: "The summary ad should start with a space, and be kept short.", type: "string" }, // TwinkleConfig.deletionSummaryAd (string) // Text to be appended to the edit summary of deletions made using Twinkle { name: "deletionSummaryAd", label: "Summary ad to use for deletion summaries", helptip: "Normally the same as the edit summary ad above.", adminOnly: true, type: "string" }, // TwinkleConfig.protectionSummaryAd (string) // Text to be appended to the edit summary of page protections made using Twinkle { name: "protectionSummaryAd", label: "Summary ad to use for page protections", helptip: "Normally the same as the edit summary ad above.", adminOnly: true, type: "string" }, // TwinkleConfig.userTalkPageMode may take arguments: // 'window': open a new window, remember the opened window // 'tab': opens in a new tab, if possible. // 'blank': force open in a new window, even if such a window exists { name: "userTalkPageMode", label: "When opening a user talk page, open it", type: "enum", enumValues: Twinkle.config.commonEnums.talkPageMode }, // TwinkleConfig.dialogLargeFont (boolean) { name: "dialogLargeFont", label: "Use larger text in Twinkle dialogs", type: "boolean" } ] }, { title: "Revert and rollback", // twinklefluff module preferences: [ // TwinkleConfig.openTalkPage (array) // What types of actions that should result in opening of talk page { name: "openTalkPage", label: "Open user talk page after these types of reversions", type: "set", setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" } }, // TwinkleConfig.openTalkPageOnAutoRevert (bool) // Defines if talk page should be opened when calling revert from contrib page, because from there, actions may be multiple, and opening talk page not suitable. If set to true, openTalkPage defines then if talk page will be opened. { name: "openTalkPageOnAutoRevert", label: "Open user talk page when invoking rollback from user contributions", helptip: "Often, you may be rolling back many pages at a time from a vandal's contributions page, so it would be unsuitable to open the user talk page. Hence, this option is off by default. When this is on, the desired options must be enabled in the previous setting for this to work.", type: "boolean" }, // TwinkleConfig.markRevertedPagesAsMinor (array) // What types of actions that should result in marking edit as minor { name: "markRevertedPagesAsMinor", label: "Mark as minor edit for these types of reversions", type: "set", setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" } }, // TwinkleConfig.watchRevertedPages (array) // What types of actions that should result in forced addition to watchlist { name: "watchRevertedPages", label: "Add pages to watchlist for these types of reversions", type: "set", setValues: { agf: "AGF rollback", norm: "Normal rollback", vand: "Vandalism rollback", torev: "\"Restore this version\"" } }, // TwinkleConfig.offerReasonOnNormalRevert (boolean) // If to offer a prompt for extra summary reason for normal reverts, default to true { name: "offerReasonOnNormalRevert", label: "Prompt for reason for normal rollbacks", helptip: "\"Normal\" rollbacks are the ones that are invoked from the middle [rollback] link.", type: "boolean" }, { name: "confirmOnFluff", label: "Provide a confirmation message before reverting", helptip: "For users of pen or touch devices, and chronically indecisive people.", type: "boolean" }, // TwinkleConfig.showRollbackLinks (array) // Where Twinkle should show rollback links (diff, others, mine, contribs) // Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres { name: "showRollbackLinks", label: "Show rollback links on these pages", type: "set", setValues: { diff: "Diff pages", others: "Contributions pages of other users", mine: "My contributions page" } } ] }, { title: "Deletion tagging", preferences: [ { name: "speedySelectionStyle", label: "When to go ahead and tag/delete the page", type: "enum", enumValues: { "buttonClick": 'When I click "Submit"', "radioClick": "As soon as I click an option" } }, // TwinkleConfig.markSpeedyPagesAsPatrolled (boolean) // If, when applying speedy template to page, to mark the page as patrolled (if the page was reached from NewPages) { name: "markSpeedyPagesAsPatrolled", label: "Mark page as patrolled when tagging (if possible)", helptip: "Due to technical limitations, pages are only marked as patrolled when they are reached via Special:NewPages.", type: "boolean" }, // TwinkleConfig.openUserTalkPageOnSpeedyDelete (array of strings) // What types of actions that should result user talk page to be opened when speedily deleting (admin only) { name: "openUserTalkPageOnSpeedyDelete", label: "Open user talk page when deleting under these criteria", adminOnly: true, type: "set", setValues: Twinkle.config.commonSets.csdAndDICriteria, setDisplayOrder: Twinkle.config.commonSets.csdAndDICriteriaDisplayOrder }, // TwinkleConfig.deleteTalkPageOnDelete (boolean) // If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only) { name: "deleteTalkPageOnDelete", label: "Check the \"also delete talk page\" box by default", adminOnly: true, type: "boolean" }, // TwinkleConfig.deleteSysopDefaultToTag (boolean) // Make the CSD screen default to "tag" instead of "delete" (admin only) { name: "deleteSysopDefaultToTag", label: "Default to tagging instead of outright deletion", adminOnly: true, type: "boolean" }, // TwinkleConfig.speedyWindowWidth (integer) // Defines the width of the Twinkle SD window in pixels { name: "speedyWindowWidth", label: "Width of deletion window (pixels)", type: "integer" }, // TwinkleConfig.speedyWindowWidth (integer) // Defines the width of the Twinkle SD window in pixels { name: "speedyWindowHeight", label: "Height of deletion window (pixels)", helptip: "If you have a big monitor, you might like to increase this.", type: "integer" }, { name: "logSpeedyNominations", label: "Keep a log in userspace of all deletion nominations", helptip: "Since non-admins do not have access to their deleted contributions, the userspace log offers a good way to keep track of all pages you nominate for QD using Twinkle. Files tagged using DI are also added to this log.", type: "boolean" }, { name: "speedyLogPageName", label: "Keep the deletion userspace log at this user subpage", helptip: "i.e. User:<i>username</i>/<i>subpage name</i>. Only works if you turn on the deletion userspace log.", type: "string" } ] }, { title: "Unlink", preferences: [ // TwinkleConfig.unlinkNamespaces (array) // In what namespaces unlink should happen, default in 0 (article) and 100 (portal) { name: "unlinkNamespaces", label: "Remove links from pages in these namespaces", helptip: "Avoid selecting any talk namespaces, as Twinkle might end up unlinking on talk archives (a big no-no).", type: "set", setValues: Twinkle.config.commonSets.namespacesNoSpecial } ] }, { title: "Hidden", hidden: true, preferences: [ // twinkle.header.js: portlet setup { name: "portletArea", type: "string" }, { name: "portletId", type: "string" }, { name: "portletName", type: "string" }, { name: "portletType", type: "string" }, { name: "portletNext", type: "string" }, // twinklefluff.js: defines how many revision to query maximum, maximum possible is 50, default is 50 { name: "revertMaxRevisions", type: "integer" }, // twinklebatchdelete.js: How many pages should be processed at a time { name: "batchdeleteChunks", type: "integer" }, // twinklebatchdelete.js: How many pages left in the process of being completed should allow a new batch to be initialized { name: "batchDeleteMinCutOff", type: "integer" }, // twinklebatchdelete.js: How many pages should be processed maximum { name: "batchMax", type: "integer" }, // twinklebatchprotect.js: How many pages should be processed at a time { name: "batchProtectChunks", type: "integer" }, // twinklebatchprotect.js: How many pages left in the process of being completed should allow a new batch to be initialized { name: "batchProtectMinCutOff", type: "integer" }, // twinklebatchundelete.js: How many pages should be processed at a time { name: "batchundeleteChunks", type: "integer" }, // twinklebatchundelete.js: How many pages left in the process of being completed should allow a new batch to be initialized { name: "batchUndeleteMinCutOff", type: "integer" } ] } ]; // end of Twinkle.config.sections //{ // name: "", // label: "", // type: "" // }, Twinkle.config.init = function twinkleconfigInit() { if (( mw.config.get("wgTitle") === "Twinkle/Preferences" || (mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user && mw.config.get("wgTitle").lastIndexOf("/Twinkle preferences") === (mw.config.get("wgTitle").length - 20))) && mw.config.get("wgAction") === "view") { // create the config page at Wikipedia:Twinkle/Preferences, and at user subpages (for testing purposes) if (!document.getElementById("twinkle-config")) { return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless } // set style (the url() CSS function doesn't seem to work from wikicode - ?!) document.getElementById("twinkle-config-titlebar").style.backgroundImage = "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB%2FqqA%2BAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEhQTFRFr73ZobTPusjdsMHZp7nVwtDhzNbnwM3fu8jdq7vUt8nbxtDkw9DhpbfSvMrfssPZqLvVztbno7bRrr7W1d%2Fs1N7qydXk0NjpkW7Q%2BgAAADVJREFUeNoMwgESQCAAAMGLkEIi%2FP%2BnbnbpdB59app5Vdg0sXAoMZCpGoFbK6ciuy6FX4ABAEyoAef0BXOXAAAAAElFTkSuQmCC)"; var contentdiv = document.getElementById("twinkle-config-content"); contentdiv.textContent = ""; // clear children // let user know about possible conflict with monobook.js/vector.js file // (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings) var contentnotice = document.createElement("p"); // I hate innerHTML, but this is one thing it *is* good for... contentnotice.innerHTML = "<b>Before modifying your preferences here,</b> make sure you have removed any old <code>TwinkleConfig</code> and <code>FriendlyConfig</code> settings from your <a href=\"" + mw.util.getUrl("Special:MyPage/skin.js") + "\" title=\"Special:MyPage/skin.js\">user JavaScript file</a>."; contentdiv.appendChild(contentnotice); // look and see if the user does in fact have any old settings in their skin JS file var skinjs = new Morebits.wiki.page("User:" + mw.config.get("wgUserName") + "/" + mw.config.get("skin") + ".js"); skinjs.setCallbackParameters(contentnotice); skinjs.load(Twinkle.config.legacyPrefsNotice); // start a table of contents var toctable = document.createElement("table"); toctable.className = "toc"; toctable.style.marginLeft = "0.4em"; var toctr = document.createElement("tr"); var toctd = document.createElement("td"); // create TOC title var toctitle = document.createElement("div"); toctitle.id = "toctitle"; var toch2 = document.createElement("h2"); toch2.textContent = "Contents "; toctitle.appendChild(toch2); // add TOC show/hide link var toctoggle = document.createElement("span"); toctoggle.className = "toctoggle"; toctoggle.appendChild(document.createTextNode("[")); var toctogglelink = document.createElement("a"); toctogglelink.className = "internal"; toctogglelink.setAttribute("href", "#tw-tocshowhide"); toctogglelink.textContent = "hide"; toctoggle.appendChild(toctogglelink); toctoggle.appendChild(document.createTextNode("]")); toctitle.appendChild(toctoggle); toctd.appendChild(toctitle); // create item container: this is what we add stuff to var tocul = document.createElement("ul"); toctogglelink.addEventListener("click", function twinkleconfigTocToggle() { var $tocul = $(tocul); $tocul.toggle(); if ($tocul.find(":visible").length) { toctogglelink.textContent = "hide"; } else { toctogglelink.textContent = "show"; } }, false); toctd.appendChild(tocul); toctr.appendChild(toctd); toctable.appendChild(toctr); contentdiv.appendChild(toctable); var tocnumber = 1; var contentform = document.createElement("form"); contentform.setAttribute("action", "javascript:void(0)"); // was #tw-save - changed to void(0) to work around Chrome issue contentform.addEventListener("submit", Twinkle.config.save, true); contentdiv.appendChild(contentform); var container = document.createElement("table"); container.style.width = "100%"; contentform.appendChild(container); $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) { return true; // i.e. "continue" in this context } var configgetter; // retrieve the live config values if (section.inFriendlyConfig) { configgetter = Twinkle.getFriendlyPref; } else { configgetter = Twinkle.getPref; } // add to TOC var tocli = document.createElement("li"); tocli.className = "toclevel-1"; var toca = document.createElement("a"); toca.setAttribute("href", "#twinkle-config-section-" + tocnumber.toString()); toca.appendChild(document.createTextNode(section.title)); tocli.appendChild(toca); tocul.appendChild(tocli); var row = document.createElement("tr"); var cell = document.createElement("td"); cell.setAttribute("colspan", "3"); var heading = document.createElement("h4"); heading.style.borderBottom = "1px solid gray"; heading.style.marginTop = "0.2em"; heading.id = "twinkle-config-section-" + (tocnumber++).toString(); heading.appendChild(document.createTextNode(section.title)); cell.appendChild(heading); row.appendChild(cell); container.appendChild(row); var rowcount = 1; // for row banding // add each of the preferences to the form $(section.preferences).each(function(prefkey, pref) { if (pref.adminOnly && !Morebits.userIsInGroup("sysop")) { return true; // i.e. "continue" in this context } row = document.createElement("tr"); row.style.marginBottom = "0.2em"; // create odd row banding if (rowcount++ % 2 === 0) { row.style.backgroundColor = "rgba(128, 128, 128, 0.1)"; } cell = document.createElement("td"); var label, input; switch (pref.type) { case "boolean": // create a checkbox cell.setAttribute("colspan", "2"); label = document.createElement("label"); input = document.createElement("input"); input.setAttribute("type", "checkbox"); input.setAttribute("id", pref.name); input.setAttribute("name", pref.name); if (configgetter(pref.name) === true) { input.setAttribute("checked", "checked"); } label.appendChild(input); label.appendChild(document.createTextNode(" " + pref.label)); cell.appendChild(label); break; case "string": // create an input box case "integer": // add label to first column cell.style.textAlign = "right"; cell.style.paddingRight = "0.5em"; label = document.createElement("label"); label.setAttribute("for", pref.name); label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); row.appendChild(cell); // add input box to second column cell = document.createElement("td"); cell.style.paddingRight = "1em"; input = document.createElement("input"); input.setAttribute("type", "text"); input.setAttribute("id", pref.name); input.setAttribute("name", pref.name); if (pref.type === "integer") { input.setAttribute("size", 6); input.setAttribute("type", "number"); input.setAttribute("step", "1"); // integers only } if (configgetter(pref.name)) { input.setAttribute("value", configgetter(pref.name)); } cell.appendChild(input); break; case "enum": // create a combo box // add label to first column // note: duplicates the code above, under string/integer cell.style.textAlign = "right"; cell.style.paddingRight = "0.5em"; label = document.createElement("label"); label.setAttribute("for", pref.name); label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); row.appendChild(cell); // add input box to second column cell = document.createElement("td"); cell.style.paddingRight = "1em"; input = document.createElement("select"); input.setAttribute("id", pref.name); input.setAttribute("name", pref.name); $.each(pref.enumValues, function(enumvalue, enumdisplay) { var option = document.createElement("option"); option.setAttribute("value", enumvalue); if (configgetter(pref.name) === enumvalue) { option.setAttribute("selected", "selected"); } option.appendChild(document.createTextNode(enumdisplay)); input.appendChild(option); }); cell.appendChild(input); break; case "set": // create a set of check boxes // add label first of all cell.setAttribute("colspan", "2"); label = document.createElement("label"); // not really necessary to use a label element here, but we do it for consistency of styling label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); var checkdiv = document.createElement("div"); checkdiv.style.paddingLeft = "1em"; var worker = function(itemkey, itemvalue) { var checklabel = document.createElement("label"); checklabel.style.marginRight = "0.7em"; checklabel.style.display = "inline-block"; var check = document.createElement("input"); check.setAttribute("type", "checkbox"); check.setAttribute("id", pref.name + "_" + itemkey); check.setAttribute("name", pref.name + "_" + itemkey); if (configgetter(pref.name) && configgetter(pref.name).indexOf(itemkey) !== -1) { check.setAttribute("checked", "checked"); } // cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...) if (pref.name === "unlinkNamespaces") { if (configgetter(pref.name) && configgetter(pref.name).indexOf(parseInt(itemkey, 10)) !== -1) { check.setAttribute("checked", "checked"); } } checklabel.appendChild(check); checklabel.appendChild(document.createTextNode(itemvalue)); checkdiv.appendChild(checklabel); }; if (pref.setDisplayOrder) { // add check boxes according to the given display order $.each(pref.setDisplayOrder, function(itemkey, item) { worker(item, pref.setValues[item]); }); } else { // add check boxes according to the order it gets fed to us (probably strict alphabetical) $.each(pref.setValues, worker); } cell.appendChild(checkdiv); break; case "customList": // add label to first column cell.style.textAlign = "right"; cell.style.paddingRight = "0.5em"; label = document.createElement("label"); label.setAttribute("for", pref.name); label.appendChild(document.createTextNode(pref.label + ":")); cell.appendChild(label); row.appendChild(cell); // add button to second column cell = document.createElement("td"); cell.style.paddingRight = "1em"; var button = document.createElement("button"); button.setAttribute("id", pref.name); button.setAttribute("name", pref.name); button.setAttribute("type", "button"); button.addEventListener("click", Twinkle.config.listDialog.display, false); // use jQuery data on the button to store the current config value $(button).data({ value: configgetter(pref.name), pref: pref, inFriendlyConfig: section.inFriendlyConfig }); button.appendChild(document.createTextNode("Edit items")); cell.appendChild(button); break; default: alert("twinkleconfig: unknown data type for preference " + pref.name); break; } row.appendChild(cell); // add help tip cell = document.createElement("td"); cell.style.fontSize = "90%"; cell.style.color = "gray"; if (pref.helptip) { cell.innerHTML = pref.helptip; } // add reset link (custom lists don't need this, as their config value isn't displayed on the form) if (pref.type !== "customList") { var resetlink = document.createElement("a"); resetlink.setAttribute("href", "#tw-reset"); resetlink.setAttribute("id", "twinkle-config-reset-" + pref.name); resetlink.addEventListener("click", Twinkle.config.resetPrefLink, false); if (resetlink.style.styleFloat) { // IE (inc. IE9) resetlink.style.styleFloat = "right"; } else { // standards resetlink.style.cssFloat = "right"; } resetlink.style.margin = "0 0.6em"; resetlink.appendChild(document.createTextNode("Reset")); cell.appendChild(resetlink); } row.appendChild(cell); container.appendChild(row); return true; }); return true; }); var footerbox = document.createElement("div"); footerbox.setAttribute("id", "twinkle-config-buttonpane"); footerbox.style.backgroundColor = "#BCCADF"; footerbox.style.padding = "0.5em"; var button = document.createElement("button"); button.setAttribute("id", "twinkle-config-submit"); button.setAttribute("type", "submit"); button.appendChild(document.createTextNode("Save changes")); footerbox.appendChild(button); var footerspan = document.createElement("span"); footerspan.className = "plainlinks"; footerspan.style.marginLeft = "2.4em"; footerspan.style.fontSize = "90%"; var footera = document.createElement("a"); footera.setAttribute("href", "#tw-reset-all"); footera.setAttribute("id", "twinkle-config-resetall"); footera.addEventListener("click", Twinkle.config.resetAllPrefs, false); footera.appendChild(document.createTextNode("Restore defaults")); footerspan.appendChild(footera); footerbox.appendChild(footerspan); contentform.appendChild(footerbox); // since all the section headers exist now, we can try going to the requested anchor if (location.hash) { location.hash = location.hash; } } else if (mw.config.get("wgNamespaceNumber") === mw.config.get("wgNamespaceIds").user) { var box = document.createElement("div"); box.setAttribute("id", "twinkle-config-headerbox"); box.style.border = "1px #f60 solid"; box.style.background = "#fed"; box.style.padding = "0.6em"; box.style.margin = "0.5em auto"; box.style.textAlign = "center"; var link; if (mw.config.get("wgTitle") === mw.config.get("wgUserName") + "/twinkleoptions.js") { // place "why not try the preference panel" notice box.style.fontWeight = "bold"; box.style.width = "80%"; box.style.borderWidth = "2px"; if (mw.config.get("wgArticleId") > 0) { // page exists box.appendChild(document.createTextNode("This page contains your Twinkle preferences. You can change them using the ")); } else { // page does not exist box.appendChild(document.createTextNode("You can customize Twinkle to suit your preferences by using the ")); } link = document.createElement("a"); link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") ); link.appendChild(document.createTextNode("Twinkle preferences panel")); box.appendChild(link); box.appendChild(document.createTextNode(", or by editing this page.")); $(box).insertAfter($("#contentSub")); } else if (mw.config.get("wgTitle").indexOf(mw.config.get("wgUserName")) === 0 && mw.config.get("wgPageName").lastIndexOf(".js") === mw.config.get("wgPageName").length - 3) { // place "Looking for Twinkle options?" notice box.style.width = "60%"; box.appendChild(document.createTextNode("If you want to set Twinkle preferences, you can use the ")); link = document.createElement("a"); link.setAttribute("href", mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").project] + ":Twinkle/Preferences") ); link.appendChild(document.createTextNode("Twinkle preferences panel")); box.appendChild(link); box.appendChild(document.createTextNode(".")); $(box).insertAfter($("#contentSub")); } } }; // Morebits.wiki.page callback from init code Twinkle.config.legacyPrefsNotice = function twinkleconfigLegacyPrefsNotice(pageobj) { var text = pageobj.getPageText(); var contentnotice = pageobj.getCallbackParameters(); if (text.indexOf("TwinkleConfig") !== -1 || text.indexOf("FriendlyConfig") !== -1) { contentnotice.innerHTML = '<table class="plainlinks ombox ombox-content"><tr><td class="mbox-image">' + '<img alt="" src="http://upload.wikimedia.org/wikipedia/en/3/38/Imbox_content.png" /></td>' + '<td class="mbox-text"><p><big><b>Before modifying your settings here,</b> you must remove your old Twinkle and Friendly settings from your personal skin JavaScript.</big></p>' + '<p>To do this, you can <a href="' + mw.config.get("wgScript") + '?title=User:' + encodeURIComponent(mw.config.get("wgUserName")) + '/' + mw.config.get("skin") + '.js&action=edit" target="_tab"><b>edit your personal JavaScript</b></a>, removing all lines of code that refer to <code>TwinkleConfig</code> and <code>FriendlyConfig</code>.</p>' + '</td></tr></table>'; } else { $(contentnotice).remove(); } }; // custom list-related stuff Twinkle.config.listDialog = {}; Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow(dlgtable, value, label) { var contenttr = document.createElement("tr"); // "remove" button var contenttd = document.createElement("td"); var removeButton = document.createElement("button"); removeButton.setAttribute("type", "button"); removeButton.addEventListener("click", function() { $(contenttr).remove(); }, false); removeButton.textContent = "Remove"; contenttd.appendChild(removeButton); contenttr.appendChild(contenttd); // value input box contenttd = document.createElement("td"); var input = document.createElement("input"); input.setAttribute("type", "text"); input.className = "twinkle-config-customlist-value"; input.style.width = "97%"; if (value) { input.setAttribute("value", value); } contenttd.appendChild(input); contenttr.appendChild(contenttd); // label input box contenttd = document.createElement("td"); input = document.createElement("input"); input.setAttribute("type", "text"); input.className = "twinkle-config-customlist-label"; input.style.width = "98%"; if (label) { input.setAttribute("value", label); } contenttd.appendChild(input); contenttr.appendChild(contenttd); dlgtable.appendChild(contenttr); }; Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) { var $prefbutton = $(e.target); var curvalue = $prefbutton.data("value"); var curpref = $prefbutton.data("pref"); var dialog = new Morebits.simpleWindow(720, 400); dialog.setTitle(curpref.label); dialog.setScriptName("Twinkle preferences"); var dialogcontent = document.createElement("div"); var dlgtable = document.createElement("table"); dlgtable.className = "wikitable"; dlgtable.style.margin = "1.4em 1em"; dlgtable.style.width = "auto"; var dlgtbody = document.createElement("tbody"); // header row var dlgtr = document.createElement("tr"); // top-left cell var dlgth = document.createElement("th"); dlgth.style.width = "5%"; dlgtr.appendChild(dlgth); // value column header dlgth = document.createElement("th"); dlgth.style.width = "35%"; dlgth.textContent = (curpref.customListValueTitle ? curpref.customListValueTitle : "Value"); dlgtr.appendChild(dlgth); // label column header dlgth = document.createElement("th"); dlgth.style.width = "60%"; dlgth.textContent = (curpref.customListLabelTitle ? curpref.customListLabelTitle : "Label"); dlgtr.appendChild(dlgth); dlgtbody.appendChild(dlgtr); // content rows var gotRow = false; $.each(curvalue, function(k, v) { gotRow = true; Twinkle.config.listDialog.addRow(dlgtbody, v.value, v.label); }); // if there are no values present, add a blank row to start the user off if (!gotRow) { Twinkle.config.listDialog.addRow(dlgtbody); } // final "add" button var dlgtfoot = document.createElement("tfoot"); dlgtr = document.createElement("tr"); var dlgtd = document.createElement("td"); dlgtd.setAttribute("colspan", "3"); var addButton = document.createElement("button"); addButton.style.minWidth = "8em"; addButton.setAttribute("type", "button"); addButton.addEventListener("click", function(e) { Twinkle.config.listDialog.addRow(dlgtbody); }, false); addButton.textContent = "Add"; dlgtd.appendChild(addButton); dlgtr.appendChild(dlgtd); dlgtfoot.appendChild(dlgtr); dlgtable.appendChild(dlgtbody); dlgtable.appendChild(dlgtfoot); dialogcontent.appendChild(dlgtable); // buttonpane buttons: [Save changes] [Reset] [Cancel] var button = document.createElement("button"); button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane button.addEventListener("click", function(e) { Twinkle.config.listDialog.save($prefbutton, dlgtbody); dialog.close(); }, false); button.textContent = "Save changes"; dialogcontent.appendChild(button); button = document.createElement("button"); button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane button.addEventListener("click", function(e) { Twinkle.config.listDialog.reset($prefbutton, dlgtbody); }, false); button.textContent = "Reset"; dialogcontent.appendChild(button); button = document.createElement("button"); button.setAttribute("type", "submit"); // so Morebits.simpleWindow puts the button in the button pane button.addEventListener("click", function(e) { dialog.close(); // the event parameter on this function seems to be broken }, false); button.textContent = "Cancel"; dialogcontent.appendChild(button); dialog.setContent(dialogcontent); dialog.display(); }; // Resets the data value, re-populates based on the new (default) value, then saves the // old data value again (less surprising behaviour) Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset(button, tbody) { // reset value on button var $button = $(button); var curpref = $button.data("pref"); var oldvalue = $button.data("value"); Twinkle.config.resetPref(curpref, $button.data("inFriendlyConfig")); // reset form var $tbody = $(tbody); $tbody.find("tr").slice(1).remove(); // all rows except the first (header) row // add the new values var curvalue = $button.data("value"); $.each(curvalue, function(k, v) { Twinkle.config.listDialog.addRow(tbody, v.value, v.label); }); // save the old value $button.data("value", oldvalue); }; Twinkle.config.listDialog.save = function twinkleconfigListDialogSave(button, tbody) { var result = []; var current = {}; $(tbody).find('input[type="text"]').each(function(inputkey, input) { if ($(input).hasClass("twinkle-config-customlist-value")) { current = { value: input.value }; } else { current.label = input.value; // exclude totally empty rows if (current.value || current.label) { result.push(current); } } }); $(button).data("value", result); }; // reset/restore defaults Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) { var wantedpref = e.target.id.substring(21); // "twinkle-config-reset-" prefix is stripped // search tactics $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) { return true; // continue: skip impossibilities } var foundit = false; $(section.preferences).each(function(prefkey, pref) { if (pref.name !== wantedpref) { return true; // continue } Twinkle.config.resetPref(pref, section.inFriendlyConfig); foundit = true; return false; // break }); if (foundit) { return false; // break } }); return false; // stop link from scrolling page }; Twinkle.config.resetPref = function twinkleconfigResetPref(pref, inFriendlyConfig) { switch (pref.type) { case "boolean": document.getElementById(pref.name).checked = (inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]); break; case "string": case "integer": case "enum": document.getElementById(pref.name).value = (inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]); break; case "set": $.each(pref.setValues, function(itemkey, itemvalue) { if (document.getElementById(pref.name + "_" + itemkey)) { document.getElementById(pref.name + "_" + itemkey).checked = ((inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name]).indexOf(itemkey) !== -1); } }); break; case "customList": $(document.getElementById(pref.name)).data("value", (inFriendlyConfig ? Twinkle.defaultConfig.friendly[pref.name] : Twinkle.defaultConfig.twinkle[pref.name])); break; default: alert("twinkleconfig: unknown data type for preference " + pref.name); break; } }; Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() { // no confirmation message - the user can just refresh/close the page to abort $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.hidden || (section.adminOnly && !Morebits.userIsInGroup("sysop"))) { return true; // continue: skip impossibilities } $(section.preferences).each(function(prefkey, pref) { if (!pref.adminOnly || Morebits.userIsInGroup("sysop")) { Twinkle.config.resetPref(pref, section.inFriendlyConfig); } }); return true; }); return false; // stop link from scrolling page }; Twinkle.config.save = function twinkleconfigSave(e) { Morebits.status.init( document.getElementById("twinkle-config-content") ); Morebits.wiki.actionCompleted.notice = "Save"; var userjs = mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").user] + ":" + mw.config.get("wgUserName") + "/twinkleoptions.js"; var wikipedia_page = new Morebits.wiki.page(userjs, "Saving preferences to " + userjs); wikipedia_page.setCallbackParameters(e.target); wikipedia_page.load(Twinkle.config.writePrefs); return false; }; Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) { var form = pageobj.getCallbackParameters(); var statelem = pageobj.getStatusElement(); // this is the object which gets serialized into JSON var newConfig = { twinkle: {}, friendly: {} }; // keeping track of all preferences that we encounter // any others that are set in the user's current config are kept // this way, preferences that this script doesn't know about are not lost // (it does mean obsolete prefs will never go away, but... ah well...) var foundTwinklePrefs = [], foundFriendlyPrefs = []; // a comparison function is needed later on // it is just enough for our purposes (i.e. comparing strings, numbers, booleans, // arrays of strings, and arrays of { value, label }) // and it is not very robust: e.g. compare([2], ["2"]) === true, and // compare({}, {}) === false, but it's good enough for our purposes here var compare = function(a, b) { if ($.isArray(a)) { if (a.length !== b.length) { return false; } var asort = a.sort(), bsort = b.sort(); for (var i = 0; asort[i]; ++i) { // comparison of the two properties of custom lists if ((typeof asort[i] === "object") && (asort[i].label !== bsort[i].label || asort[i].value !== bsort[i].value)) { return false; } else if (asort[i].toString() !== bsort[i].toString()) { return false; } } return true; } else { return a === b; } }; $(Twinkle.config.sections).each(function(sectionkey, section) { if (section.adminOnly && !Morebits.userIsInGroup("sysop")) { return; // i.e. "continue" in this context } // reach each of the preferences from the form $(section.preferences).each(function(prefkey, pref) { var userValue; // = undefined // only read form values for those prefs that have them if (!section.hidden && (!pref.adminOnly || Morebits.userIsInGroup("sysop"))) { switch (pref.type) { case "boolean": // read from the checkbox userValue = form[pref.name].checked; break; case "string": // read from the input box or combo box case "enum": userValue = form[pref.name].value; break; case "integer": // read from the input box userValue = parseInt(form[pref.name].value, 10); if (isNaN(userValue)) { Morebits.status.warn("Saving", "The value you specified for " + pref.name + " (" + pref.value + ") was invalid. The save will continue, but the invalid data value will be skipped."); userValue = null; } break; case "set": // read from the set of check boxes userValue = []; if (pref.setDisplayOrder) { // read only those keys specified in the display order $.each(pref.setDisplayOrder, function(itemkey, item) { if (form[pref.name + "_" + item].checked) { userValue.push(item); } }); } else { // read all the keys in the list of values $.each(pref.setValues, function(itemkey, itemvalue) { if (form[pref.name + "_" + itemkey].checked) { userValue.push(itemkey); } }); } break; case "customList": // read from the jQuery data stored on the button object userValue = $(form[pref.name]).data("value"); break; default: alert("twinkleconfig: unknown data type for preference " + pref.name); break; } } // only save those preferences that are *different* from the default if (section.inFriendlyConfig) { if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.friendly[pref.name])) { newConfig.friendly[pref.name] = userValue; } foundFriendlyPrefs.push(pref.name); } else { if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig.twinkle[pref.name])) { newConfig.twinkle[pref.name] = userValue; } foundTwinklePrefs.push(pref.name); } }); }); if (Twinkle.prefs) { $.each(Twinkle.prefs.twinkle, function(tkey, tvalue) { if (foundTwinklePrefs.indexOf(tkey) === -1) { newConfig.twinkle[tkey] = tvalue; } }); $.each(Twinkle.prefs.friendly, function(fkey, fvalue) { if (foundFriendlyPrefs.indexOf(fkey) === -1) { newConfig.friendly[fkey] = fvalue; } }); } var text = "// twinkleoptions.js: personal Twinkle preferences file\n" + "//\n" + "// NOTE: The easiest way to change your Twinkle preferences is by using the\n" + "// Twinkle preferences panel, at [[" + mw.config.get("wgPageName") + "]].\n" + "//\n" + "// This file is AUTOMATICALLY GENERATED. Any changes you make (aside from\n" + "// changing the configuration parameters in a valid-JavaScript way) will be\n" + "// overwritten the next time you click \"save\" in the Twinkle preferences\n" + "// panel. If modifying this file, make sure to use correct JavaScript.\n" + "\n" + "window.Twinkle.prefs = "; text += JSON.stringify(newConfig, null, 2); text += ";\n" + "\n" + "// End of twinkleoptions.js\n"; pageobj.setPageText(text); pageobj.setEditSummary("Saving Twinkle preferences: automatic edit from [[" + mw.config.get("wgPageName") + "]]"); pageobj.setCreateOption("recreate"); pageobj.save(Twinkle.config.saveSuccess); }; Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) { pageobj.getStatusElement().info("successful"); var noticebox = document.createElement("div"); noticebox.className = "successbox"; noticebox.style.fontSize = "100%"; noticebox.style.marginTop = "2em"; noticebox.innerHTML = "<p><b>Your Twinkle preferences have been saved.</b></p><p>To see the changes, you will need to <b>clear your browser cache entirely</b> (see <a href=\"" + mw.util.getUrl("WP:BYPASS") + "\" title=\"WP:BYPASS\">WP:BYPASS</a> for instructions).</p>"; Morebits.status.root.appendChild(noticebox); var noticeclear = document.createElement("br"); noticeclear.style.clear = "both"; Morebits.status.root.appendChild(noticeclear); }; /* **************************************** *** twinklediff.js: Diff module **************************************** * Mode of invocation: Tab on non-diff pages ("Last"); tabs on diff pages ("Since", "Since mine", "Current") * Active on: Existing non-special pages * Config directives in: TwinkleConfig */ Twinkle.diff = function twinklediff() { if( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') ) { return; } var query = { 'title': mw.config.get('wgPageName'), 'diff': 'cur', 'oldid': 'prev' }; twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Last', 'tw-lastdiff', 'Show most recent diff' ); // Show additional tabs only on diff pages if (Morebits.queryString.exists('diff')) { twAddPortletLink(function(){ Twinkle.diff.evaluate(false); }, 'Since', 'tw-since', 'Show difference between last diff and the revision made by previous user' ); twAddPortletLink( function(){ Twinkle.diff.evaluate(true); }, 'Since mine', 'tw-sincemine', 'Show difference between last diff and my last revision' ); var oldid = /oldid=(.+)/.exec($('#mw-diff-ntitle1').find('strong a').first().attr("href"))[1]; query = { 'title': mw.config.get('wgPageName'), 'diff': 'cur', 'oldid' : oldid }; twAddPortletLink( mw.util.wikiScript("index")+ "?" + $.param( query ), 'Current', 'tw-curdiff', 'Show difference to current revision' ); } }; Twinkle.diff.evaluate = function twinklediffEvaluate(me) { var user; if( me ) { user = mw.config.get('wgUserName'); } else { var node = document.getElementById( 'mw-diff-ntitle2' ); if( ! node ) { // nothing to do? return; } user = $(node).find('a').first().text(); } var query = { 'prop': 'revisions', 'action': 'query', 'titles': mw.config.get('wgPageName'), 'rvlimit': 1, 'rvprop': [ 'ids', 'user' ], 'rvstartid': mw.config.get('wgCurRevisionId') - 1, // i.e. not the current one 'rvuser': user }; Morebits.status.init( document.getElementById('bodyContent') ); var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of initial contributor', query, Twinkle.diff.callbacks.main ); wikipedia_api.params = { user: user }; wikipedia_api.post(); }; Twinkle.diff.callbacks = { main: function( self ) { var xmlDoc = self.responseXML; var revid = $(xmlDoc).find('rev').attr('revid'); if( ! revid ) { self.statelem.error( 'no suitable earlier revision found, or ' + self.params.user + ' is the only contributor. Aborting.' ); return; } var query = { 'title': mw.config.get('wgPageName'), 'oldid': revid, 'diff': mw.config.get('wgCurRevisionId') }; window.location = mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ); } }; /* **************************************** *** twinklefluff.js: Revert/rollback module **************************************** * Mode of invocation: Links on history, contributions, and diff pages * Active on: Diff pages, history pages, contributions pages * Config directives in: TwinkleConfig */ /** Twinklefluff revert and antivandalism utility */ Twinkle.fluff = { auto: function() { if( parseInt( Morebits.queryString.get('oldid'), 10) !== mw.config.get('wgCurRevisionId') ) { // not latest revision alert("Can't rollback, page has changed in the meantime."); return; } var vandal = $("#mw-diff-ntitle2").find("a.mw-userlink").text(); Twinkle.fluff.revert( Morebits.queryString.get( 'twinklerevert' ), vandal, true ); }, normal: function() { var spanTag = function( color, content ) { var span = document.createElement( 'span' ); span.style.color = color; span.appendChild( document.createTextNode( content ) ); return span; }; if( mw.config.get('wgNamespaceNumber') === -1 && mw.config.get('wgCanonicalSpecialPageName') === "Contributions" ) { //Get the username these contributions are for var lastLogNode = $('#contentSub').find('a[title^="Special:Log"]').last(); if(!lastLogNode) return; var logMatch = /wiki\/Special:Log\/(.+)$/.exec( lastLogNode ? lastLogNode.attr("href").replace(/_/g, "%20") : '' ); if(!logMatch) return; username = decodeURIComponent(logMatch[1]); if( Twinkle.getPref('showRollbackLinks').indexOf('contribs') !== -1 || ( mw.config.get('wgUserName') !== username && Twinkle.getPref('showRollbackLinks').indexOf('others') !== -1 ) || ( mw.config.get('wgUserName') === username && Twinkle.getPref('showRollbackLinks').indexOf('mine') !== -1 ) ) { var list = $("#bodyContent").find("ul li:has(span.mw-uctop)"); var revNode = document.createElement('strong'); var revLink = document.createElement('a'); revLink.appendChild( spanTag( 'Black', '[' ) ); revLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) ); revLink.appendChild( spanTag( 'Black', ']' ) ); revNode.appendChild(revLink); var revVandNode = document.createElement('strong'); var revVandLink = document.createElement('a'); revVandLink.appendChild( spanTag( 'Black', '[' ) ); revVandLink.appendChild( spanTag( 'Red', 'vandalism' ) ); revVandLink.appendChild( spanTag( 'Black', ']' ) ); revVandNode.appendChild(revVandLink); list.each(function(key, current) { var href = $(current).children("a:eq(1)").attr("href"); current.appendChild( document.createTextNode(' ') ); var tmpNode = revNode.cloneNode( true ); tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'norm' } ) ); current.appendChild( tmpNode ); current.appendChild( document.createTextNode(' ') ); tmpNode = revVandNode.cloneNode( true ); tmpNode.firstChild.setAttribute( 'href', href + '&' + Morebits.queryString.create( { 'twinklerevert': 'vand' } ) ); current.appendChild( tmpNode ); }); } } else { if( mw.config.get('wgCanonicalSpecialPageName') === "Undelete" ) { //You can't rollback deleted pages! return; } var body = document.getElementById('bodyContent'); var firstRev = $("div.firstrevisionheader").length; if( firstRev ) { // we have first revision here, nothing to do. return; } var otitle, ntitle; try { var otitle1 = document.getElementById('mw-diff-otitle1'); var ntitle1 = document.getElementById('mw-diff-ntitle1'); if (!otitle1 || !ntitle1) { return; } otitle = otitle1.parentNode; ntitle = ntitle1.parentNode; } catch( e ) { // no old, nor new title, nothing to do really, return; return; } var old_rev_url = $("#mw-diff-otitle1").find("strong a").attr("href"); // Lets first add a [edit this revision] link var query = new Morebits.queryString( old_rev_url.split( '?', 2 )[1] ); var oldrev = query.get('oldid'); var revertToRevision = document.createElement('div'); revertToRevision.setAttribute( 'id', 'tw-revert-to-orevision' ); revertToRevision.style.fontWeight = 'bold'; var revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') ); revertToRevisionLink.href = "#"; $(revertToRevisionLink).click(function(){ Twinkle.fluff.revertToRevision(oldrev); }); revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) ); revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) ); revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) ); otitle.insertBefore( revertToRevision, otitle.firstChild ); if( document.getElementById('differences-nextlink') ) { // Not latest revision curVersion = false; var new_rev_url = $("#mw-diff-ntitle1").find("strong a").attr("href"); query = new Morebits.queryString( new_rev_url.split( '?', 2 )[1] ); var newrev = query.get('oldid'); revertToRevision = document.createElement('div'); revertToRevision.setAttribute( 'id', 'tw-revert-to-nrevision' ); revertToRevision.style.fontWeight = 'bold'; revertToRevisionLink = revertToRevision.appendChild( document.createElement('a') ); revertToRevisionLink.href = "#"; $(revertToRevisionLink).click(function(){ Twinkle.fluff.revertToRevision(newrev); }); revertToRevisionLink.appendChild( spanTag( 'Black', '[' ) ); revertToRevisionLink.appendChild( spanTag( 'SaddleBrown', 'restore this version' ) ); revertToRevisionLink.appendChild( spanTag( 'Black', ']' ) ); ntitle.insertBefore( revertToRevision, ntitle.firstChild ); return; } if( Twinkle.getPref('showRollbackLinks').indexOf('diff') !== -1 ) { var vandal = $("#mw-diff-ntitle2").find("a").first().text(); var revertNode = document.createElement('div'); revertNode.setAttribute( 'id', 'tw-revert' ); var agfNode = document.createElement('strong'); var vandNode = document.createElement('strong'); var normNode = document.createElement('strong'); var agfLink = document.createElement('a'); var vandLink = document.createElement('a'); var normLink = document.createElement('a'); agfLink.href = "#"; vandLink.href = "#"; normLink.href = "#"; $(agfLink).click(function(){ Twinkle.fluff.revert('agf', vandal); }); $(vandLink).click(function(){ Twinkle.fluff.revert('vand', vandal); }); $(normLink).click(function(){ Twinkle.fluff.revert('norm', vandal); }); agfLink.appendChild( spanTag( 'Black', '[' ) ); agfLink.appendChild( spanTag( 'DarkOliveGreen', 'rollback (AGF)' ) ); agfLink.appendChild( spanTag( 'Black', ']' ) ); vandLink.appendChild( spanTag( 'Black', '[' ) ); vandLink.appendChild( spanTag( 'Red', 'rollback (VANDAL)' ) ); vandLink.appendChild( spanTag( 'Black', ']' ) ); normLink.appendChild( spanTag( 'Black', '[' ) ); normLink.appendChild( spanTag( 'SteelBlue', 'rollback' ) ); normLink.appendChild( spanTag( 'Black', ']' ) ); agfNode.appendChild(agfLink); vandNode.appendChild(vandLink); normNode.appendChild(normLink); revertNode.appendChild( agfNode ); revertNode.appendChild( document.createTextNode(' || ') ); revertNode.appendChild( normNode ); revertNode.appendChild( document.createTextNode(' || ') ); revertNode.appendChild( vandNode ); ntitle.insertBefore( revertNode, ntitle.firstChild ); } } } }; Twinkle.fluff.revert = function revertPage( type, vandal, autoRevert, rev, page ) { if (mw.util.isIPv6Address(vandal)) { vandal = Morebits.sanitizeIPv6(vandal); } var pagename = page || mw.config.get('wgPageName'); var revid = rev || mw.config.get('wgCurRevisionId'); Morebits.status.init( document.getElementById('bodyContent') ); var params = { type: type, user: vandal, pagename: pagename, revid: revid, autoRevert: !!autoRevert }; var query = { 'action': 'query', 'prop': ['info', 'revisions'], 'titles': pagename, 'rvlimit': 50, // max possible 'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ], 'curtimestamp': '', 'meta': 'tokens', 'type': 'csrf', }; var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of earlier revisions', query, Twinkle.fluff.callbacks.main ); wikipedia_api.params = params; wikipedia_api.post(); }; Twinkle.fluff.revertToRevision = function revertToRevision( oldrev ) { Morebits.status.init( document.getElementById('bodyContent') ); var query = { 'action': 'query', 'prop': ['info', 'revisions'], 'titles': mw.config.get('wgPageName'), 'rvlimit': 1, 'rvstartid': oldrev, 'rvprop': [ 'ids', 'timestamp', 'user', 'comment' ], 'curtimestamp': '', 'meta': 'tokens', 'type': 'csrf', 'format': 'xml' }; var wikipedia_api = new Morebits.wiki.api( 'Grabbing data of the earlier revision', query, Twinkle.fluff.callbacks.toRevision.main ); wikipedia_api.params = { rev: oldrev }; wikipedia_api.post(); }; Twinkle.fluff.userIpLink = function( user ) { return (Morebits.isIPAddress(user) ? "[[Special:Contributions/" : "[[User:" ) + user + "|" + user + "]]"; }; Twinkle.fluff.callbacks = { toRevision: { main: function( self ) { var xmlDoc = self.responseXML; var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10); var touched = $(xmlDoc).find('page').attr('touched'); var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp'); var edittoken = $(xmlDoc).find('tokens').attr('csrftoken'); var revertToRevID = $(xmlDoc).find('rev').attr('revid'); var revertToUser = $(xmlDoc).find('rev').attr('user'); if (revertToRevID !== self.params.rev) { self.statitem.error( 'The retrieved revision does not match the requested revision. Aborting.' ); return; } var optional_summary = prompt( "Please specify a reason for the revert: ", "" ); // padded out to widen prompt in Firefox if (optional_summary === null) { self.statelem.error( 'Aborted by user.' ); return; } var summary = "Reverted to revision " + revertToRevID + " by " + revertToUser + (optional_summary ? ": " + optional_summary : '') + "." + Twinkle.getPref('summaryAd'); var query = { 'action': 'edit', 'title': mw.config.get('wgPageName'), 'summary': summary, 'token': edittoken, 'undo': lastrevid, 'undoafter': revertToRevID, 'basetimestamp': touched, 'starttimestamp': starttimestamp, 'watchlist': Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined, 'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined }; Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Reversion completed"; var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, null/*Twinkle.fluff.callbacks.toRevision.complete*/, self.statelem); wikipedia_api.params = self.params; wikipedia_api.post(); }, complete: function (self) { } }, main: function( self ) { var xmlDoc = self.responseXML; var lastrevid = parseInt( $(xmlDoc).find('page').attr('lastrevid'), 10); var touched = $(xmlDoc).find('page').attr('touched'); var starttimestamp = $(xmlDoc).find('api').attr('curtimestamp'); var edittoken = $(xmlDoc).find('tokens').attr('csrftoken'); var lastuser = $(xmlDoc).find('rev').attr('user'); var revs = $(xmlDoc).find('rev'); if( revs.length < 1 ) { self.statelem.error( 'We have less than one additional revision, thus impossible to revert' ); return; } var top = revs[0]; if( lastrevid < self.params.revid ) { Morebits.status.error( 'Error', [ 'The most recent revision ID received from the server, ', Morebits.htmlNode( 'strong', lastrevid ), ', is less than the ID of the displayed revision. This could indicate that the current revision has been deleted, the server is lagging, or that bad data has been received. Will stop proceeding at this point.' ] ); return; } var index = 1; if( self.params.revid !== lastrevid ) { Morebits.status.warn( 'Warning', [ 'Latest revision ', Morebits.htmlNode( 'strong', lastrevid ), ' doesn\'t equal our revision ', Morebits.htmlNode( 'strong', self.params.revid ) ] ); if( lastuser === self.params.user ) { switch( self.params.type ) { case 'vand': Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume vandalism, we continue to revert' ]); break; case 'agf': Morebits.status.warn( 'Warning', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , '. As we assume good faith, we stop reverting, as the problem might have been fixed.' ]); return; default: Morebits.status.warn( 'Notice', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', self.params.user ) , ', but we will stop reverting anyway.' ] ); return; } } else if(self.params.type === 'vand' && Twinkle.fluff.whiteList.indexOf( top.getAttribute( 'user' ) ) !== -1 && revs.length > 1 && revs[1].getAttribute( 'pageId' ) === self.params.revid) { Morebits.status.info( 'Info', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', a trusted bot, and the revision before was made by our vandal, so we proceed with the revert.' ] ); index = 2; } else { Morebits.status.error( 'Error', [ 'Latest revision was made by ', Morebits.htmlNode( 'strong', lastuser ), ', so it might have already been reverted, stopping reverting.'] ); return; } } if( Twinkle.fluff.whiteList.indexOf( self.params.user ) !== -1 ) { switch( self.params.type ) { case 'vand': Morebits.status.info( 'Info', [ 'Vandalism revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. As this is a whitelisted bot, we assume you wanted to revert vandalism made by the previous user instead.' ] ); index = 2; vandal = revs[1].getAttribute( 'user' ); self.params.user = revs[1].getAttribute( 'user' ); break; case 'agf': Morebits.status.warn( 'Notice', [ 'Good faith revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, it makes no sense at all to revert it as a good faith edit, will stop reverting.' ] ); return; case 'norm': /* falls through */ default: var cont = confirm( 'Normal revert was chosen, but the most recent edit was made by a whitelisted bot (' + self.params.user + '). Do you want to revert the revision before instead?' ); if( cont ) { Morebits.status.info( 'Info', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, and per confirmation, we\'ll revert the previous revision instead.' ] ); index = 2; self.params.user = revs[1].getAttribute( 'user' ); } else { Morebits.status.warn( 'Notice', [ 'Normal revert was chosen on ', Morebits.htmlNode( 'strong', self.params.user ), '. This is a whitelisted bot, but per confirmation, revert on top revision will proceed.' ] ); } break; } } var found = false; var count = 0; for( var i = index; i < revs.length; ++i ) { ++count; if( revs[i].getAttribute( 'user' ) !== self.params.user ) { found = i; break; } } if( ! found ) { self.statelem.error( [ 'No previous revision found. Perhaps ', Morebits.htmlNode( 'strong', self.params.user ), ' is the only contributor, or that the user has made more than ' + Twinkle.getPref('revertMaxRevisions') + ' edits in a row.' ] ); return; } if( ! count ) { Morebits.status.error( 'Error', "We were to revert zero revisions. As that makes no sense, we'll stop reverting this time. It could be that the edit has already been reverted, but the revision ID was still the same." ); return; } var good_revision = revs[ found ]; var userHasAlreadyConfirmedAction = false; if (self.params.type !== 'vand' && count > 1) { if ( !confirm( self.params.user + ' has made ' + count + ' edits in a row. Are you sure you want to revert them all?') ) { Morebits.status.info( 'Notice', 'Stopping reverting per user input' ); return; } userHasAlreadyConfirmedAction = true; } self.params.count = count; self.params.goodid = good_revision.getAttribute( 'revid' ); self.params.gooduser = good_revision.getAttribute( 'user' ); self.statelem.status( [ ' revision ', Morebits.htmlNode( 'strong', self.params.goodid ), ' that was made ', Morebits.htmlNode( 'strong', count ), ' revisions ago by ', Morebits.htmlNode( 'strong', self.params.gooduser ) ] ); var summary, extra_summary, userstr, gooduserstr; switch( self.params.type ) { case 'agf': extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox if (extra_summary === null) { self.statelem.error( 'Aborted by user.' ); return; } userHasAlreadyConfirmedAction = true; userstr = self.params.user; summary = "Reverted good faith edits by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) + Twinkle.getPref('summaryAd'); break; case 'vand': userstr = self.params.user; gooduserstr = self.params.gooduser; summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]]) identified as vandalism to last revision by " + gooduserstr + "." + Twinkle.getPref('summaryAd'); break; case 'norm': /* falls through */ default: if( Twinkle.getPref('offerReasonOnNormalRevert') ) { extra_summary = prompt( "An optional comment for the edit summary: ", "" ); // padded out to widen prompt in Firefox if (extra_summary === null) { self.statelem.error( 'Aborted by user.' ); return; } userHasAlreadyConfirmedAction = true; } userstr = self.params.user; summary = "Reverted " + self.params.count + (self.params.count > 1 ? ' edits' : ' edit') + " by [[Special:Contributions/" + userstr + "|" + userstr + "]] ([[User talk:" + userstr + "|talk]])" + Twinkle.fluff.formatSummaryPostfix(extra_summary) + Twinkle.getPref('summaryAd'); break; } if (Twinkle.getPref('confirmOnFluff') && !userHasAlreadyConfirmedAction && !confirm("Reverting page: are you sure?")) { self.statelem.error( 'Aborted by user.' ); return; } var query; if( (!self.params.autoRevert || Twinkle.getPref('openTalkPageOnAutoRevert')) && Twinkle.getPref('openTalkPage').indexOf( self.params.type ) !== -1 && mw.config.get('wgUserName') !== self.params.user ) { Morebits.status.info( 'Info', [ 'Opening user talk page edit form for user ', Morebits.htmlNode( 'strong', self.params.user ) ] ); query = { 'title': 'User talk:' + self.params.user, 'action': 'edit', 'preview': 'yes', 'vanarticle': self.params.pagename.replace(/_/g, ' '), 'vanarticlerevid': self.params.revid, 'vanarticlegoodrevid': self.params.goodid, 'type': self.params.type, 'count': self.params.count }; switch( Twinkle.getPref('userTalkPageMode') ) { case 'tab': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' ); break; case 'blank': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; case 'window': /* falls through */ default: window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; } } query = { 'action': 'edit', 'title': self.params.pagename, 'summary': summary, 'token': edittoken, 'undo': lastrevid, 'undoafter': self.params.goodid, 'basetimestamp': touched, 'starttimestamp': starttimestamp, 'watchlist' : Twinkle.getPref('watchRevertedPages').indexOf( self.params.type ) !== -1 ? 'watch' : undefined, 'minor': Twinkle.getPref('markRevertedPagesAsMinor').indexOf( self.params.type ) !== -1 ? true : undefined }; Morebits.wiki.actionCompleted.redirect = self.params.pagename; Morebits.wiki.actionCompleted.notice = "Reversion completed"; var wikipedia_api = new Morebits.wiki.api( 'Saving reverted contents', query, Twinkle.fluff.callbacks.complete, self.statelem); wikipedia_api.params = self.params; wikipedia_api.post(); }, complete: function (self) { self.statelem.info("done"); } }; Twinkle.fluff.formatSummaryPostfix = function(stringToAdd) { if (stringToAdd) { stringToAdd = ': ' + Morebits.string.toUpperCaseFirstChar(stringToAdd); if (stringToAdd.search(/[.?!;]$/) === -1) { stringToAdd = stringToAdd + '.'; } return stringToAdd; } else { return '.'; } }; Twinkle.fluff.init = function twinklefluffinit() { if (twinkleUserAuthorized) { // a list of usernames, usually only bots, that vandalism revert is jumped over, that is // if vandalism revert was chosen on such username, then it's target is on the revision before. // This is for handeling quick bots that makes edits seconds after the original edit is made. // This only affect vandalism rollback, for good faith rollback, it will stop, indicating a bot // has no faith, and for normal rollback, it will rollback that edit. Twinkle.fluff.whiteList = [ 'AnomieBOT', 'ClueBot NG', 'SineBot' ]; if ( Morebits.queryString.exists( 'twinklerevert' ) ) { Twinkle.fluff.auto(); } else { Twinkle.fluff.normal(); } } }; /* **************************************** *** twinklespeedy.js: CSD module **************************************** * Mode of invocation: Tab ("CSD") * Active on: Non-special, existing pages * Config directives in: TwinkleConfig * * NOTE FOR DEVELOPERS: * If adding a new criterion, check out the default values of the CSD preferences * in twinkle.header.js, and add your new criterion to those if you think it would * be good. */ Twinkle.speedy = function twinklespeedy() { // Disable on: // * special pages // * non-existent pages if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) { return; } twAddPortletLink( Twinkle.speedy.callback, "Del", "tw-csd", Morebits.userIsInGroup('sysop') ? "Delete page" : "Request deletion" ); }; // This function is run when the CSD tab/header link is clicked Twinkle.speedy.callback = function twinklespeedyCallback() { if ( !twinkleUserAuthorized ) { alert("Your account is too new to use Twinkle."); return; } Twinkle.speedy.initDialog(Morebits.userIsInGroup( 'sysop' ) ? Twinkle.speedy.callback.evaluateSysop : Twinkle.speedy.callback.evaluateUser, true); }; Twinkle.speedy.dialog = null; // used by unlink feature // Prepares the speedy deletion dialog and displays it Twinkle.speedy.initDialog = function twinklespeedyInitDialog(callbackfunc) { var dialog; Twinkle.speedy.dialog = new Morebits.simpleWindow( Twinkle.getPref('speedyWindowWidth'), Twinkle.getPref('speedyWindowHeight') ); dialog = Twinkle.speedy.dialog; dialog.setTitle( "Choose criteria for deletion" ); dialog.setScriptName( "Twinkle" ); //dialog.addFooterLink( "Quick deletion policy", "Wikipedia:Deletion policy#Quick deletion" ); dialog.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#speedy" ); var form = new Morebits.quickForm( callbackfunc, (Twinkle.getPref('speedySelectionStyle') === 'radioClick' ? 'change' : null) ); if( Morebits.userIsInGroup( 'sysop' ) ) { form.append( { type: 'checkbox', list: [ { label: 'Tag page only, don\'t delete', value: 'tag_only', name: 'tag_only', tooltip: 'If you just want to tag the page, instead of deleting it now', checked : Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { var cForm = event.target.form; var cChecked = event.target.checked; // enable/disable talk page checkbox if (cForm.talkpage) { cForm.talkpage.disabled = cChecked; cForm.talkpage.checked = !cChecked && Twinkle.getPref('deleteTalkPageOnDelete'); } // enable/disable redirects checkbox cForm.redirects.disabled = cChecked; cForm.redirects.checked = !cChecked; // enable/disable notify checkbox cForm.notify.disabled = !cChecked; cForm.notify.checked = cChecked; // enable/disable multiple cForm.multiple.disabled = !cChecked; cForm.multiple.checked = false; Twinkle.speedy.callback.dbMultipleChanged(cForm, false); event.stopPropagation(); } } ] } ); form.append( { type: 'header', label: 'Delete-related options' } ); if (mw.config.get('wgNamespaceNumber') % 2 === 0 && (mw.config.get('wgNamespaceNumber') !== 2 || (/\//).test(mw.config.get('wgTitle')))) { // hide option for user pages, to avoid accidentally deleting user talk page form.append( { type: 'checkbox', list: [ { label: 'Also delete talk page', value: 'talkpage', name: 'talkpage', tooltip: "This option deletes the page's talk page in addition. If you choose the F8 (moved to Commons) criterion, this option is ignored and the talk page is *not* deleted.", checked: Twinkle.getPref('deleteTalkPageOnDelete'), disabled: Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { event.stopPropagation(); } } ] } ); } form.append( { type: 'checkbox', list: [ { label: 'Also delete all redirects', value: 'redirects', name: 'redirects', tooltip: "This option deletes all incoming redirects in addition. Avoid this option for procedural (e.g. move/merge) deletions.", checked: true, disabled: Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { event.stopPropagation(); } } ] } ); form.append( { type: 'header', label: 'Tag-related options' } ); } form.append( { type: 'checkbox', list: [ { label: 'Notify page creator if possible', value: 'notify', name: 'notify', tooltip: "A notification template will be placed on the talk page of the creator, IF you have a notification enabled in your Twinkle preferences " + "for the criterion you choose AND this box is checked. The creator may be welcomed as well.", checked: !Morebits.userIsInGroup( 'sysop' ) || Twinkle.getPref('deleteSysopDefaultToTag'), disabled: Morebits.userIsInGroup( 'sysop' ) && !Twinkle.getPref('deleteSysopDefaultToTag'), event: function( event ) { event.stopPropagation(); } } ] } ); form.append( { type: 'div', name: 'work_area', label: 'Failed to initialize the CSD module. Please try again, or tell the Twinkle developers about the issue.' } ); if( Twinkle.getPref( 'speedySelectionStyle' ) !== 'radioClick' ) { form.append( { type: 'submit' } ); } var result = form.render(); dialog.setContent( result ); dialog.display(); Twinkle.speedy.callback.dbMultipleChanged( result, false ); }; Twinkle.speedy.callback.dbMultipleChanged = function twinklespeedyCallbackDbMultipleChanged(form, checked) { var namespace = mw.config.get('wgNamespaceNumber'); var value = checked; var work_area = new Morebits.quickForm.element( { type: 'div', name: 'work_area' } ); if (checked && Twinkle.getPref('speedySelectionStyle') === 'radioClick') { work_area.append( { type: 'div', label: 'When finished choosing criteria, click:' } ); work_area.append( { type: 'button', name: 'submit-multiple', label: 'Submit Query', event: function( event ) { Twinkle.speedy.callback.evaluateUser( event ); event.stopPropagation(); } } ); } var radioOrCheckbox = (value ? 'checkbox' : 'radio'); /* if (namespace % 2 === 1 && namespace !== 3) { // talk pages, but not user talk pages work_area.append( { type: 'header', label: 'Talk pages' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.talkList } ); } switch (namespace) { case 0: // article case 1: // talk work_area.append( { type: 'header', label: 'Articles' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getArticleList(value) } ); break; case 2: // user case 3: // user talk work_area.append( { type: 'header', label: 'User pages' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.userList } ); break; case 6: // file case 7: // file talk work_area.append( { type: 'header', label: 'Files' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getFileList(value) } ); break; case 10: // template case 11: // template talk work_area.append( { type: 'header', label: 'Templates' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.templateList } ); break; case 14: // category case 15: // category talk work_area.append( { type: 'header', label: 'Categories' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.categoryList } ); break; default: break; } */ work_area.append( { type: 'header', label: 'General criteria' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.getGeneralList(value) }); /* work_area.append( { type: 'header', label: 'Redirects' } ); work_area.append( { type: radioOrCheckbox, name: 'csd', list: Twinkle.speedy.redirectList } ); */ var old_area = Morebits.quickForm.getElements(form, "work_area")[0]; form.replaceChild(work_area.render(), old_area); }; Twinkle.speedy.talkList = [ /*{ label: 'G8: Talk pages with no page belonging to it', value: 'talk', tooltip: 'This does not include any page that is useful to the project - for example user talk pages, talk page archives, and talk pages for files that exist on Wikimedia Commons.' }*/ ]; // this is a function to allow for db-multiple filtering Twinkle.speedy.getFileList = function twinklespeedyGetFileList(multiple) { var result = []; /*result.push({ label: 'F1: Not allowed', value: 'prohibitedimage', tooltip: 'Most media uploads are not allowed on Simple English Wikipedia. They should be uploaded to Wikimedia Commons instead. There are a few exceptions to this rule. Firstly, all spoken articles should be uploaded here, as they are for local use. Secondly, there are some logos that Commons does not accept, but are needed here, for example Image:Wiki.png, which is used as the Wikipedia logo.' });*/ return result; }; Twinkle.speedy.getArticleList = function twinklespeedyGetArticleList(multiple) { var result = []; /*result.push({ label: 'A1: Little or no meaning', value: 'nocontext', tooltip: 'Is very short and providing little or no meaning (e.g., "He is a funny man that has created Factory and the Hacienda. And, by the way, his wife is great."). Having a small amount of content is not a reason to delete if it has useful information.' }); result.push({ label: 'A2: No content', value: 'nocontent', tooltip: 'Has no content. This includes any article consisting only of links (including hyperlinks, category tags and "see also" sections), a rephrasing of the title, and/or attempts to correspond with the person or group named by its title. This does not include disambiguation pages.' }); result.push({ label: 'A3: Article that exists on another Wikimedia project', value: 'transwiki', tooltip: 'Has been copied and pasted from another Wikipedia: Any article or section from an article that has been copied and pasted with little or no change.' }); result.push({ label: 'A4: People, groups, companies, products, services or websites that do not claim to be notable.', value: 'notability', tooltip: 'An article about a real person, group of people, band, club, company, product, service or or web content that does not say why it is important. If not everyone agrees that the subject is not notable or there has been a previous RfD, the article may not be quickly deleted, and should be discussed at RfD instead.' }); result.push({ label: 'A5: Not written in English', value: 'foreign', tooltip: 'Any article that is not written in English. An article that is written in any other languages but English.' }); result.push({ label: 'A6: Obvious hoax', value: 'hoax', tooltip: 'Is an obvious hoax. An article that is surely fake or impossible.' });*/ return result; }; Twinkle.speedy.categoryList = [ /*{ label: 'C1: Empty categories', value: 'catempty', tooltip: '(with no articles or subcategories for at least four days) whose only content includes links to parent categories. However, this can not be used on categories still being discussed on WP:RfD, or disambiguation categories. If the category wasn\'t newly made, it is possible that it used to have articles, and more inspection is needed.' }, { label: 'C2: Quick renaming', value: 'catqr', tooltip: 'Empty categories that have already been renamed.' }, { label: 'C3: Template categories', value: 'catfd', tooltip: 'If a category contains articles from only one template (such as Category:Cleanup needed from \{\{cleanup\}\}) and the template is deleted after being discussed, the category can also be deleted without being discussed.' }*/ ]; Twinkle.speedy.userList = [ /*{ label: 'U1: User request', value: 'userreq', tooltip: 'User pages can be deleted if its user wants to, but there are some exceptions.' }, { label: 'U2: Nonexistent user', value: 'nouser', tooltip: 'User pages of users that do not exist. Administrators should check Special:Contributions and Special:DeletedContributions.' }*/ ]; Twinkle.speedy.templateList = [ /*{ label: 'T2: They are deprecated or replaced by a newer template and are completely unused and not linked to.', value: 'replaced', tooltip: 'For any template that should not be deleted quickly, use Wikipedia:Requests for deletion.' }*/ //}); // return result; ]; Twinkle.speedy.getGeneralList = function twinklespeedyGetGeneralList(multiple) { var result = []; if (!multiple) { result.push({ label: 'Custom rationale' + (Morebits.userIsInGroup('sysop') ? ' (custom deletion reason)' : ' using {'+'{delete|reason}}'), value: 'reason', tooltip: 'You can enter an custom reason.' }); } /*result.push({ label: 'G1: Nonsense', value: 'nonsense', tooltip: 'All of the text is nonsense. Nonsense includes content that does not make sense or is not meaningful. However, this does not include bad writing, bad words, vandalism, things that are fake or impossible, or parts which are not in English. ' }); result.push({ label: 'G2: Test page', value: 'test', tooltip: 'It is a test page, such as "Can I really create a page here?".' }); result.push({ label: 'G3: Complete vandalism', value: 'vandalism', tooltip: 'The content is completely vandalism.' }); result.push({ label: 'G4: Recreation of deleted material already deleted at RfD', value: 'repost', tooltip: 'Creation of content that is already deleted. It includes an identical or similar copy, with any title, of a page that was deleted, after being discussed in Requests for deletion, unless it was undeleted due to another discussion or was recreated in the user space. Before deleting again, the Administrator should be sure that the content is similar and not just a new article on the same subject. This rule cannot be used if the content had already been quickly deleted before.' }); if (!multiple) { result.push({ label: 'G6: History merge', value: 'histmerge', tooltip: 'Temporarily deleting a page in order to merge page histories' }); result.push({ label: 'G6: Move', value: 'move', tooltip: 'Making way for a noncontroversial move like reversing a redirect' }); result.push({ label: 'G6: RfD', value: 'afd', tooltip: 'An admin has closed a RfD as "delete".' }); } result.push({ label: 'G6: Housekeeping', value: 'g6', tooltip: 'Other non-controversial "housekeeping" tasks' }); result.push({ label: 'G7: Author requests deletion, or author blanked', value: 'author', tooltip: 'Any page whose original author wants deletion, can be quickly deleted, but only if most of the page was written by that author and was created as a mistake. If the author blanks the page, this can mean that he or she wants it deleted.' }); result.push({ label: 'G8: Pages dependent on a non-existent or deleted page', value: 'talk', tooltip: '... can be deleted, unless they contain discussion on deletion that can\'t be found anywhere else. Subpages of a talk page can only be deleted under this rule if their top-level page does not exist. This also applies to broken redirects. However, this cannot be used on user talk pages or talk pages of images on Commons.' }); if (!multiple) { result.push({ label: 'G8: Subpages with no parent page', value: 'subpage', tooltip: 'This excludes any page that is useful to the project, and in particular: deletion discussions that are not logged elsewhere, user and user talk pages, talk page archives, plausible redirects that can be changed to valid targets, and file pages or talk pages for files that exist on Wikimedia Commons.' }); } result.push({ label: 'G10: Attack page', value: 'attack', tooltip: 'Pages that were only created to insult a person or thing (such as "John Q. Doe is dumb"). This includes articles on a living person that is insult and without sources, where there is no NPOV version in the edit history to revert to.' }); result.push({ label: 'G11: Obvious advertising', value: 'spam', tooltip: 'Pages which were created only to say good things about a company, item, group or service and which would need to be written again so that they can sound like an encyclopedia. However, simply having a company, item, group or service as its subject does not mean that an article can be deleted because of this rule: an article that is obvious advertising should have content that shouldn\'t be in an encyclopedia. If a page has already gone through RfD or QD and was not deleted, it should not be quickly deleted using this rule.' }); result.push({ label: 'G12: Obviously breaking copyright law', value: 'copyvio', tooltip: 'Obviously breaking copyright law like a page which is 1) Copied from another website which does not have a license that can be used with Wikipedia; 2) Containing no content in the page history that is worth being saved. 3) Made by one person instead of being created on wiki and then copied by another website such as one of the many Wikipedia mirror websites. 4) Added by someone who doesn\'t tell if he got permission to do so or not, or if his claim has a large chance of not being true;' });*/ return result; }; Twinkle.speedy.redirectList = [ /*{ label: 'R1: Redirects to a non-existent page.', value: 'redirnone', tooltip: 'Redirects to a non-existent page.' }, { label: 'R2: Redirects from mainspace to any other namespace except the Category:, Template:, Wikipedia:, Help: and Portal: namespaces', value: 'rediruser', tooltip: '(this does not include the Wikipedia shortcut pseudo-namespaces). If this was the result of a page move, consider waiting a day or two before deleting the redirect' }, { label: 'R3: Redirects as a result of an implausible typo that were recently created', value: 'redirtypo', tooltip: 'However, redirects from common misspellings or misnomers are generally useful, as are redirects in other languages' }*/ ]; Twinkle.speedy.normalizeHash = { 'reason': 'db', 'nonsense': 'g1', 'test': 'g2', 'vandalism': 'g3', 'hoax': 'g3', 'repost': 'g4', 'histmerge': 'g6', 'move': 'g6', 'afd': 'g6', 'g6': 'g6', 'author': 'g7', 'talk': 'g8', 'subpage': 'g8', 'attack': 'g10', 'spam': 'g11', 'copyvio': 'g12', 'nocontext': 'a1', 'nocontent': 'a2', 'transwiki': 'a3', 'notability': 'a4', 'foreign': 'a5', 'hoax': 'a6', 'redirnone': 'r1', 'rediruser': 'r2', 'redirtypo': 'r3', 'prohibitedimage': 'f1', 'catempty': 'c1', 'catqr': 'c2', 'catfd': 'c3', 'userreq': 'u1', 'nouser': 'u2', 'replaced':'t2' }; // keep this synched with [[MediaWiki:Deletereason-dropdown]] Twinkle.speedy.reasonHash = { 'reason': '', 'nonsense': 'was all nonsense', 'test': 'was a test page', 'vandalism': 'was vandalism', 'pagemove': 'was a redirect created during cleanup of page move vandalism', 'repost': 'was a copy of a page that was deleted by RfD', 'histmerge': 'was in the way of trying to fix or clean up something', 'move': 'was in the way of making a move', 'afd': 'was closed as delete in a RfD', 'g6': 'was housekeeping', 'author': 'was asked to be deleted by the author', 'blanked': 'was implied to be deleted by the author', 'talk': 'was a talk page of a page that does not exist', 'attack': 'was an attack page', 'spam': 'was advertising', 'copyvio': 'was breaking copyright law', 'nocontext': 'was a page that had little or no meaning', 'nocontent': 'was a page that had no content', 'transwiki': 'was copied from another Wikipedia', 'notability': 'was a page that didn\'t say why the subject was notable', 'foreign': 'was not written in English', 'hoax': 'was obviously a hoax (not true)', 'redirnone': 'was a redirect to a page that does not exist', 'rediruser': 'was a redirect to the Talk:, User: or User talk: space', 'redirtypo': 'was a redirect with an uncommon typo', 'prohibitedimage': 'was an image/media that is not allowed on Wikipedia', 'catempty': 'was an empty category', 'catqr': 'was a renamed category', 'catfd': 'was a category containing articles from a now deleted template', 'userreq': 'was a user page whose user requested deletion', 'nouser': 'was a user page of a user that did not exist', 'replaced': 'was deprecated or replaced by a newer template and are completely unused and not linked to' }; Twinkle.speedy.callbacks = { sysop: { main: function( params ) { var thispage = new Morebits.wiki.page( mw.config.get('wgPageName'), "Deleting page" ); // delete page var reason; if (params.normalized === 'db') { reason = prompt("Enter the deletion summary to use, which will be entered into the deletion log:", ""); } else { var presetReason = "[[WP:QD#" + params.normalized.toUpperCase() + "|" + params.normalized.toUpperCase() + "]]: " + params.reason; if (Twinkle.getPref("promptForSpeedyDeletionSummary").indexOf(params.normalized) !== -1) { reason = prompt("Enter the deletion summary to use, or press OK to accept the automatically generated one.", presetReason); } else { reason = presetReason; } } if (!reason || !reason.replace(/^\s*/, "").replace(/\s*$/, "")) { Morebits.status.error("Asking for reason", "you didn't give one. I don't know... what with admins and their apathetic antics... I give up..."); return; } thispage.setEditSummary( reason + Twinkle.getPref('deletionSummaryAd') ); thispage.deletePage(); // delete talk page if (params.deleteTalkPage && params.normalized !== 'f8' && document.getElementById( 'ca-talk' ).className !== 'new') { var talkpage = new Morebits.wiki.page( Morebits.wikipedia.namespaces[ mw.config.get('wgNamespaceNumber') + 1 ] + ':' + mw.config.get('wgTitle'), "Deleting talk page" ); talkpage.setEditSummary('Talk page of deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd')); talkpage.deletePage(); } // promote Unlink tool var $link, $bigtext; if( mw.config.get('wgNamespaceNumber') === 6 && params.normalized !== 'f8' ) { $link = $('<a/>', { 'href': '#', 'text': 'click here to go to the Unlink tool', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' }, 'click': function(){ Morebits.wiki.actionCompleted.redirect = null; Twinkle.speedy.dialog.close(); Twinkle.unlink.callback("Removing usages of and/or links to deleted file " + mw.config.get('wgPageName')); } }); $bigtext = $('<span/>', { 'text': 'To orphan backlinks and remove instances of file usage', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' } }); Morebits.status.info($bigtext[0], $link[0]); } else if (params.normalized !== 'f8') { $link = $('<a/>', { 'href': '#', 'text': 'click here to go to the Unlink tool', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' }, 'click': function(){ Morebits.wiki.actionCompleted.redirect = null; Twinkle.speedy.dialog.close(); Twinkle.unlink.callback("Removing links to deleted page " + mw.config.get('wgPageName')); } }); $bigtext = $('<span/>', { 'text': 'To orphan backlinks', 'css': { 'fontSize': '130%', 'fontWeight': 'bold' } }); Morebits.status.info($bigtext[0], $link[0]); } // open talk page of first contributor if( params.openusertalk ) { thispage = new Morebits.wiki.page( mw.config.get('wgPageName') ); // a necessary evil, in order to clear incorrect status text thispage.setCallbackParameters( params ); thispage.lookupCreator( Twinkle.speedy.callbacks.sysop.openUserTalkPage ); } // delete redirects if (params.deleteRedirects) { var query = { 'action': 'query', 'list': 'backlinks', 'blfilterredir': 'redirects', 'bltitle': mw.config.get('wgPageName'), 'bllimit': 5000 // 500 is max for normal users, 5000 for bots and sysops }; var wikipedia_api = new Morebits.wiki.api( 'getting list of redirects...', query, Twinkle.speedy.callbacks.sysop.deleteRedirectsMain, new Morebits.status( 'Deleting redirects' ) ); wikipedia_api.params = params; wikipedia_api.post(); } }, openUserTalkPage: function( pageobj ) { pageobj.getStatusElement().unlink(); // don't need it anymore var user = pageobj.getCreator(); var statusIndicator = new Morebits.status('Opening user talk page edit form for ' + user, 'opening...'); var query = { 'title': 'User talk:' + user, 'action': 'edit', 'preview': 'yes', 'vanarticle': mw.config.get('wgPageName').replace(/_/g, ' ') }; switch( Twinkle.getPref('userTalkPageMode') ) { case 'tab': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_tab' ); break; case 'blank': window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), '_blank', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; case 'window': /* falls through */ default : window.open( mw.util.wikiScript('index') + '?' + Morebits.queryString.create( query ), 'twinklewarnwindow', 'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800' ); break; } statusIndicator.info( 'complete' ); }, deleteRedirectsMain: function( apiobj ) { var xmlDoc = apiobj.getXML(); var $snapshot = $(xmlDoc).find('backlinks bl'); var total = $snapshot.length; if( !total ) { return; } var statusIndicator = apiobj.statelem; statusIndicator.status("0%"); var onsuccess = function( apiobj ) { var obj = apiobj.params.obj; var total = apiobj.params.total; var now = parseInt( 100 * ++(apiobj.params.current)/total, 10 ) + '%'; obj.update( now ); apiobj.statelem.unlink(); if( apiobj.params.current >= total ) { obj.info( now + ' (completed)' ); Morebits.wiki.removeCheckpoint(); } }; Morebits.wiki.addCheckpoint(); var params = $.extend( {}, apiobj.params ); params.current = 0; params.total = total; params.obj = statusIndicator; $snapshot.each(function(key, value) { var title = $(value).attr('title'); var page = new Morebits.wiki.page(title, 'Deleting redirect "' + title + '"'); page.setEditSummary('Redirect to deleted page "' + mw.config.get('wgPageName') + '"' + Twinkle.getPref('deletionSummaryAd')); page.deletePage(onsuccess); }); } }, user: { main: function(pageobj) { var statelem = pageobj.getStatusElement(); if (!pageobj.exists()) { statelem.error( "It seems that the page doesn't exist; perhaps it has already been deleted" ); return; } var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); statelem.status( 'Checking for tags on the page...' ); // check for existing deletion tags var tag = /(?:\{\{\s*(qd|qd-multiple|db|delete|db-.*?)(?:\s*\||\s*\}\}))/.exec( text ); if( tag ) { statelem.error( [ Morebits.htmlNode( 'strong', tag[1] ) , " is already placed on the page." ] ); return; } var xfd = /(?:\{\{([rsaiftcm]fd|md1|proposed deletion)[^{}]*?\}\})/i.exec( text ); if( xfd && !confirm( "The deletion-related template {{" + xfd[1] + "}} was found on the page. Do you still want to add a CSD template?" ) ) { return; } var code, parameters, i; if (params.normalizeds.length > 1) { code = "{{QD-multiple"; var breakFlag = false; $.each(params.normalizeds, function(index, norm) { code += "|" + norm.toUpperCase(); parameters = Twinkle.speedy.getParameters(params.values[index], norm, statelem); if (!parameters) { breakFlag = true; return false; // the user aborted } for (i in parameters) { if (typeof parameters[i] === 'string' && !parseInt(i, 10)) { // skip numeric parameters - {{db-multiple}} doesn't understand them code += "|" + i + "=" + parameters[i]; } } }); if (breakFlag) { return; } code += "}}"; params.utparams = []; } else { parameters = Twinkle.speedy.getParameters(params.values[0], params.normalizeds[0], statelem); if (!parameters) { return; // the user aborted } code = "{{delete|" + params.normalizeds; for (i in parameters) { if (typeof parameters[i] === 'string') { code += "|" + i + "=" + parameters[i]; } } code += "|editor=" + mw.config.get("wgUserName") + "|date=~~~~~"; code += "}}"; params.utparams = Twinkle.speedy.getUserTalkParameters(params.normalizeds[0], parameters); } var thispage = new Morebits.wiki.page(mw.config.get('wgPageName')); // patrol the page, if reached from Special:NewPages if( Twinkle.getPref('markSpeedyPagesAsPatrolled') ) { thispage.patrol(); } // Wrap SD template in noinclude tags if we are in template space. // Won't work with userboxes in userspace, or any other transcluded page outside template space if (mw.config.get('wgNamespaceNumber') === 10) { // Template: code = "<noinclude>" + code + "</noinclude>"; } // Remove tags that become superfluous with this action if (mw.config.get('wgNamespaceNumber') === 6) { // remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons text = text.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)[^}]*\}\}/gi, ""); } // Generate edit summary for edit var editsummary; if (params.normalizeds.length > 1) { editsummary = 'Requesting quick deletion ('; $.each(params.normalizeds, function(index, norm) { editsummary += '[[WP:QD#' + norm.toUpperCase() + '|QD ' + norm.toUpperCase() + ']], '; }); editsummary = editsummary.substr(0, editsummary.length - 2); // remove trailing comma editsummary += ').'; } else if (params.normalizeds[0] === "db") { editsummary = 'Requesting deletion with criteria \"' + parameters["1"] + '\".'; } else if (params.values[0] === "histmerge") { editsummary = "Requesting history merge with [[" + parameters["1"] + "]]"; } else { editsummary = "Requesting deletion (" + params.normalizeds[0].toUpperCase() + ")"; } pageobj.setPageText(code + ((params.normalizeds.indexOf('g10') !== -1) ? '' : ("\n" + text) )); // cause attack pages to be blanked pageobj.setEditSummary(editsummary + Twinkle.getPref('summaryAd')); pageobj.setWatchlist(params.watch); pageobj.setCreateOption('nocreate'); pageobj.save(Twinkle.speedy.callbacks.user.tagComplete); }, tagComplete: function(pageobj) { var params = pageobj.getCallbackParameters(); // Notification to first contributor if (params.usertalk) { var callback = function(pageobj) { var initialContrib = pageobj.getCreator(); // don't notify users when their user talk page is nominated if (initialContrib === mw.config.get('wgTitle') && mw.config.get('wgNamespaceNumber') === 3) { Morebits.status.warn("Notifying initial contributor: this user created their own user talk page; skipping notification"); return; } var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")"), notifytext, i; // specialcase "db" and "db-multiple" if (params.normalizeds.length > 1) { notifytext = "\n{{subst:QD-notice-multiple|page=" + mw.config.get('wgPageName'); var count = 2; $.each(params.normalizeds, function(index, norm) { notifytext += "|" + (count++) + "=" + norm.toUpperCase(); }); } else if (params.normalizeds[0] === "db") { notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds; } else { notifytext = "\n{{subst:QD-notice|page=" + mw.config.get('wgPageName') + "|cat=" + params.normalizeds; } for (i in params.utparams) { if (typeof params.utparams[i] === 'string') { notifytext += "|" + i + "=" + params.utparams[i]; } } notifytext += (params.welcomeuser ? "" : "|nowelcome=yes") + "}} ~~~~"; usertalkpage.setAppendText(notifytext); usertalkpage.setEditSummary("Notification: quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); usertalkpage.setCreateOption('recreate'); usertalkpage.setFollowRedirect(true); usertalkpage.append(); // add this nomination to the user's userspace log, if the user has enabled it if (params.lognomination) { Twinkle.speedy.callbacks.user.addToLog(params, initialContrib); } }; var thispage = new Morebits.wiki.page(mw.config.get('wgPageName')); thispage.lookupCreator(callback); } // or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name else if (params.lognomination) { Twinkle.speedy.callbacks.user.addToLog(params, null); } }, // note: this code is also invoked from twinkleimage // the params used are: // for CSD: params.values, params.normalizeds (note: normalizeds is an array) // for DI: params.fromDI = true, params.type, params.normalized (note: normalized is a string) addToLog: function(params, initialContrib) { var wikipedia_page = new Morebits.wiki.page("User:" + mw.config.get('wgUserName') + "/" + Twinkle.getPref('speedyLogPageName'), "Adding entry to userspace log"); params.logInitialContrib = initialContrib; wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.speedy.callbacks.user.saveLog); }, saveLog: function(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); // add blurb if log page doesn't exist if (!pageobj.exists()) { text = "This is a log of all deletion nominations made by this user using [[mh:dev:Twinkle|Twinkle]]'s QD module.\n\n" + "If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and " + "request deletion for this page.\n"; if (Morebits.userIsInGroup("sysop")) { text += "\nThis log does not track outright speedy deletions made using Twinkle.\n"; } } // create monthly header var date = new Date(); var headerRe = new RegExp("^==+\\s*" + date.getUTCMonthName() + "\\s+" + date.getUTCFullYear() + "\\s*==+", "m"); if (!headerRe.exec(text)) { text += "\n\n=== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==="; } text += "\n# [[:" + mw.config.get('wgPageName') + "]]: "; if (params.fromDI) { text += "DI [[WP:QD#" + params.normalized.toUpperCase() + "|QD " + params.normalized.toUpperCase() + "]] (" + params.type + ")"; } else { if (params.normalizeds.length > 1) { text += "multiple criteria ("; $.each(params.normalizeds, function(index, norm) { text += "[[WP:QD#" + norm.toUpperCase() + "|" + norm.toUpperCase() + ']], '; }); text = text.substr(0, text.length - 2); // remove trailing comma text += ')'; } else if (params.normalizeds[0] === "db") { text += "{{tl|QD}}"; } else { text += "[[WP:QD#" + params.normalizeds[0].toUpperCase() + "|CSD " + params.normalizeds[0].toUpperCase() + "]] ({{tl|db-" + params.values[0] + "}})"; } } if (params.logInitialContrib) { text += "; notified {{user|" + params.logInitialContrib + "}}"; } text += " ~~~~~\n"; pageobj.setPageText(text); pageobj.setEditSummary("Logging quick deletion nomination of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); pageobj.setCreateOption("recreate"); pageobj.save(); } } }; // prompts user for parameters to be passed into the speedy deletion tag Twinkle.speedy.getParameters = function twinklespeedyGetParameters(value, normalized, statelem) { var parameters = []; switch( normalized ) { case 'db': var dbrationale = prompt('Please enter a custom reason. \n\"This page can be quickly deleted because:\"', ""); if (!dbrationale || !dbrationale.replace(/^\s*/, "").replace(/\s*$/, "")) { statelem.error( 'You must specify a reason. Aborted by user.' ); return null; } parameters["1"] = dbrationale; break; case 'g12': var url = prompt( '[QD G12] Please enter the URL if available, including the "http://":', "" ); if (url === null) { statelem.error( 'Aborted by user.' ); return null; } parameters.url = url; break; default: var defaultreason = prompt('You can enter more details here. \n' + "Just click OK if you don't want or need to.", ""); if (defaultreason === null) { return true; // continue to next tag } else if (defaultreason !== "") { parameters["2"] = defaultreason; } break; } return parameters; }; // function for processing talk page notification template parameters Twinkle.speedy.getUserTalkParameters = function twinklespeedyGetUserTalkParameters(normalized, parameters) { var utparams = []; switch (normalized) { case 'db': utparams["2"] = parameters["1"]; break; case 'a10': utparams.key1 = "article"; utparams.value1 = parameters.article; break; default: break; } return utparams; }; Twinkle.speedy.resolveCsdValues = function twinklespeedyResolveCsdValues(e) { var values = (e.target.form ? e.target.form : e.target).getChecked('csd'); if (values.length === 0) { alert( "Please select a criterion!" ); return null; } return values; }; Twinkle.speedy.callback.evaluateSysop = function twinklespeedyCallbackEvaluateSysop(e) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var form = (e.target.form ? e.target.form : e.target); var tag_only = form.tag_only; if( tag_only && tag_only.checked ) { Twinkle.speedy.callback.evaluateUser(e); return; } var value = Twinkle.speedy.resolveCsdValues(e)[0]; if (!value) { return; } var normalized = Twinkle.speedy.normalizeHash[ value ]; var params = { value: value, normalized: normalized, watch: Twinkle.getPref('watchSpeedyPages').indexOf( normalized ) !== -1, reason: Twinkle.speedy.reasonHash[ value ], openusertalk: Twinkle.getPref('openUserTalkPageOnSpeedyDelete').indexOf( normalized ) !== -1, deleteTalkPage: form.talkpage && form.talkpage.checked, deleteRedirects: form.redirects.checked }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Twinkle.speedy.callbacks.sysop.main( params ); }; Twinkle.speedy.callback.evaluateUser = function twinklespeedyCallbackEvaluateUser(e) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var form = (e.target.form ? e.target.form : e.target); if (e.target.type === "checkbox") { return; } var values = Twinkle.speedy.resolveCsdValues(e); if (!values) { return; } //var multiple = form.multiple.checked; var normalizeds = []; $.each(values, function(index, value) { var norm = Twinkle.speedy.normalizeHash[ value ]; // for sysops only if (['f4', 'f5', 'f6', 'f11'].indexOf(norm) !== -1) { alert("Tagging with F4, F5, F6, and F11 is not possible using the CSD module. Try using DI instead, or unchecking \"Tag page only\" if you meant to delete the page."); return; } normalizeds.push(norm); }); // analyse each criterion to determine whether to watch the page/notify the creator var watchPage = false; $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('watchSpeedyPages').indexOf(norm) !== -1) { watchPage = true; return false; // break } }); var notifyuser = false; if (form.notify.checked) { $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('notifyUserOnSpeedyDeletionNomination').indexOf(norm) !== -1) { notifyuser = true; return false; // break } }); } var welcomeuser = false; if (notifyuser) { $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('welcomeUserOnSpeedyDeletionNotification').indexOf(norm) !== -1) { welcomeuser = true; return false; // break } }); } var csdlog = false; if (Twinkle.getPref('logSpeedyNominations')) { $.each(normalizeds, function(index, norm) { if (Twinkle.getPref('noLogOnSpeedyNomination').indexOf(norm) === -1) { csdlog = true; return false; // break } }); } var params = { values: values, normalizeds: normalizeds, watch: watchPage, usertalk: notifyuser, welcomeuser: welcomeuser, lognomination: csdlog }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( form ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Tagging complete"; var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Tagging page"); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.speedy.callbacks.user.main); }; /* **************************************** *** twinkleunlink.js: Unlink module **************************************** * Mode of invocation: Tab ("Unlink") * Active on: Non-special pages * Config directives in: TwinkleConfig */ Twinkle.unlink = function twinkleunlink() { if( mw.config.get('wgNamespaceNumber') < 0 ) { return; } twAddPortletLink( Twinkle.unlink.callback, "Unlink", "tw-unlink", "Unlink backlinks" ); }; Twinkle.unlink.getChecked2 = function twinkleunlinkGetChecked2( nodelist ) { if( !( nodelist instanceof NodeList ) && !( nodelist instanceof HTMLCollection ) ) { return nodelist.checked ? [ nodelist.values ] : []; } var result = []; for(var i = 0; i < nodelist.length; ++i ) { if( nodelist[i].checked ) { result.push( nodelist[i].values ); } } return result; }; // the parameter is used when invoking unlink from admin speedy Twinkle.unlink.callback = function(presetReason) { var Window = new Morebits.simpleWindow( 800, 400 ); Window.setTitle( "Unlink backlinks" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#unlink" ); var form = new Morebits.quickForm( Twinkle.unlink.callback.evaluate ); form.append( { type: 'textarea', name: 'reason', label: 'Reason: ', value: (presetReason ? presetReason : '') } ); var query; if(mw.config.get('wgNamespaceNumber') === 6) { // File: query = { 'action': 'query', 'list': [ 'backlinks', 'imageusage' ], 'bltitle': mw.config.get('wgPageName'), 'iutitle': mw.config.get('wgPageName'), 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops 'iulimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops 'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages. }; } else { query = { 'action': 'query', 'list': 'backlinks', 'bltitle': mw.config.get('wgPageName'), 'blfilterredir': 'nonredirects', 'bllimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500, // 500 is max for normal users, 5000 for bots and sysops 'blnamespace': Twinkle.getPref('unlinkNamespaces') // Main namespace and portal namespace only, keep on talk pages. }; } var wikipedia_api = new Morebits.wiki.api( 'Grabbing backlinks', query, Twinkle.unlink.callbacks.display.backlinks ); wikipedia_api.params = { form: form, Window: Window, image: mw.config.get('wgNamespaceNumber') === 6 }; wikipedia_api.post(); var root = document.createElement( 'div' ); root.style.padding = '15px'; // just so it doesn't look broken Morebits.status.init( root ); wikipedia_api.statelem.status( "loading..." ); Window.setContent( root ); Window.display(); }; Twinkle.unlink.callback.evaluate = function twinkleunlinkCallbackEvaluate(event) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! Twinkle.unlink.backlinksdone = 0; Twinkle.unlink.imageusagedone = 0; function processunlink(pages, imageusage) { var statusIndicator = new Morebits.status((imageusage ? 'Unlinking instances of file usage' : 'Unlinking backlinks'), '0%'); var total = pages.length; // removing doubling of this number - no apparent reason for it Morebits.wiki.addCheckpoint(); if( !pages.length ) { statusIndicator.info( '100% (completed)' ); Morebits.wiki.removeCheckpoint(); return; } // get an edit token var params = { reason: reason, imageusage: imageusage, globalstatus: statusIndicator, current: 0, total: total }; for (var i = 0; i < pages.length; ++i) { var myparams = $.extend({}, params); var articlepage = new Morebits.wiki.page(pages[i], 'Unlinking in article "' + pages[i] + '"'); articlepage.setCallbackParameters(myparams); articlepage.load(imageusage ? Twinkle.unlink.callbacks.unlinkImageInstances : Twinkle.unlink.callbacks.unlinkBacklinks); } } var reason = event.target.reason.value; var backlinks, imageusage; if( event.target.backlinks ) { backlinks = Twinkle.unlink.getChecked2(event.target.backlinks); } if( event.target.imageusage ) { imageusage = Twinkle.unlink.getChecked2(event.target.imageusage); } Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( event.target ); Morebits.wiki.addCheckpoint(); if (backlinks) { processunlink(backlinks, false); } if (imageusage) { processunlink(imageusage, true); } Morebits.wiki.removeCheckpoint(); }; Twinkle.unlink.backlinksdone = 0; Twinkle.unlink.imageusagedone = 0; Twinkle.unlink.callbacks = { display: { backlinks: function twinkleunlinkCallbackDisplayBacklinks(apiobj) { var xmlDoc = apiobj.responseXML; var havecontent = false; var list, namespaces, i; if( apiobj.params.image ) { var imageusage = $(xmlDoc).find('query imageusage iu'); list = []; for ( i = 0; i < imageusage.length; ++i ) { var usagetitle = imageusage[i].getAttribute('title'); list.push( { label: usagetitle, value: usagetitle, checked: true } ); } if (!list.length) { apiobj.params.form.append( { type: 'div', label: 'No instances of file usage found.' } ); } else { apiobj.params.form.append( { type:'header', label: 'File usage' } ); namespaces = []; $.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) { namespaces.push(Morebits.wikipedia.namespacesFriendly[v]); }); apiobj.params.form.append( { type: 'div', label: "Selected namespaces: " + namespaces.join(', '), tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]" }); if ($(xmlDoc).find('query-continue').length) { apiobj.params.form.append( { type: 'div', label: "First " + list.length.toString() + " file usages shown." }); } apiobj.params.form.append( { type: 'checkbox', name: 'imageusage', list: list } ); havecontent = true; } } var backlinks = $(xmlDoc).find('query backlinks bl'); if( backlinks.length > 0 ) { list = []; for ( i = 0; i < backlinks.length; ++i ) { var title = backlinks[i].getAttribute('title'); list.push( { label: title, value: title, checked: true } ); } apiobj.params.form.append( { type:'header', label: 'Backlinks' } ); namespaces = []; $.each(Twinkle.getPref('unlinkNamespaces'), function(k, v) { namespaces.push(Morebits.wikipedia.namespacesFriendly[v]); }); apiobj.params.form.append( { type: 'div', label: "Selected namespaces: " + namespaces.join(', '), tooltip: "You can change this with your Twinkle preferences, at [[Project:Twinkle/Preferences]]" }); if ($(xmlDoc).find('query-continue').length) { apiobj.params.form.append( { type: 'div', label: "First " + list.length.toString() + " backlinks shown." }); } apiobj.params.form.append( { type: 'checkbox', name: 'backlinks', list: list }); havecontent = true; } else { apiobj.params.form.append( { type: 'div', label: 'No backlinks found.' } ); } if (havecontent) { apiobj.params.form.append( { type:'submit' } ); } var result = apiobj.params.form.render(); apiobj.params.Window.setContent( result ); } }, unlinkBacklinks: function twinkleunlinkCallbackUnlinkBacklinks(pageobj) { var text, oldtext; text = oldtext = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var wikiPage = new Morebits.wikitext.page(text); wikiPage.removeLink(mw.config.get('wgPageName')); text = wikiPage.getText(); if (text === oldtext) { // Nothing to do, return Twinkle.unlink.callbacks.success(pageobj); Morebits.wiki.actionCompleted(); return; } pageobj.setPageText(text); pageobj.setEditSummary("Removing link(s) to \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd')); pageobj.setCreateOption('nocreate'); pageobj.save(Twinkle.unlink.callbacks.success); }, unlinkImageInstances: function twinkleunlinkCallbackUnlinkImageInstances(pageobj) { var text, oldtext; text = oldtext = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var wikiPage = new Morebits.wikitext.page(text); wikiPage.commentOutImage(mw.config.get('wgTitle'), 'Commented out'); text = wikiPage.getText(); if (text === oldtext) { // Nothing to do, return Twinkle.unlink.callbacks.success(pageobj); Morebits.wiki.actionCompleted(); return; } pageobj.setPageText(text); pageobj.setEditSummary("Commenting out use(s) of file \"" + mw.config.get('wgPageName') + "\": " + params.reason + "." + Twinkle.getPref('summaryAd')); pageobj.setCreateOption('nocreate'); pageobj.save(Twinkle.unlink.callbacks.success); }, success: function twinkleunlinkCallbackSuccess(pageobj) { var params = pageobj.getCallbackParameters(); var total = params.total; var now = parseInt( 100 * (params.imageusage ? ++(Twinkle.unlink.imageusagedone) : ++(Twinkle.unlink.backlinksdone))/total, 10 ) + '%'; params.globalstatus.update( now ); if((params.imageusage ? Twinkle.unlink.imageusagedone : Twinkle.unlink.backlinksdone) >= total) { params.globalstatus.info( now + ' (completed)' ); Morebits.wiki.removeCheckpoint(); } } }; /* **************************************** *** twinklewarn.js: Warn module **************************************** * Mode of invocation: Tab ("Warn") * Active on: User talk pages * Config directives in: TwinkleConfig */ Twinkle.warn = function twinklewarn() { if( mw.config.get('wgNamespaceNumber') === 3 ) { //twAddPortletLink( Twinkle.warn.callback, "Warn", "tw-warn", "Warn/notify user" ); } // modify URL of talk page on rollback success pages if( mw.config.get('wgAction') === 'rollback' ) { var $vandalTalkLink = $("#mw-rollback-success").find(".mw-usertoollinks a").first(); $vandalTalkLink.css("font-weight", "bold"); $vandalTalkLink.wrapInner($("<span/>").attr("title", "If appropriate, you can use Twinkle to warn the user about their edits to this page.")); var extraParam = "vanarticle=" + mw.util.rawurlencode(mw.config.get("wgPageName").replace(/_/g, " ")); var href = $vandalTalkLink.attr("href"); if (href.indexOf("?") === -1) { $vandalTalkLink.attr("href", href + "?" + extraParam); } else { $vandalTalkLink.attr("href", href + "&" + extraParam); } } }; Twinkle.warn.callback = function twinklewarnCallback() { if ( !twinkleUserAuthorized ) { alert("Your account is too new to use Twinkle."); return; } if( mw.config.get('wgTitle').split( '/' )[0] === mw.config.get('wgUserName') && !confirm( 'Warning yourself can be seen as a sign of mental instability! Are you sure you want to proceed?' ) ) { return; } var Window = new Morebits.simpleWindow( 600, 440 ); Window.setTitle( "Warn/notify user" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "User talk page warnings", "Template:User_talk_page_warnings#Warnings_and_notices" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#warn" ); var form = new Morebits.quickForm( Twinkle.warn.callback.evaluate ); var main_select = form.append( { type:'field', label:'Choose type of warning/notice to issue', tooltip:'First choose a main warning group, then the specific warning to issue.' } ); var main_group = main_select.append( { type:'select', name:'main_group', event:Twinkle.warn.callback.change_category } ); var defaultGroup = parseInt(Twinkle.getPref('defaultWarningGroup'), 10); main_group.append( { type:'option', label:'General note (1)', value:'level1', selected: ( defaultGroup === 1 || defaultGroup < 1 || ( Morebits.userIsInGroup( 'sysop' ) ? defaultGroup > 8 : defaultGroup > 7 ) ) } ); main_group.append( { type:'option', label:'Caution (2)', value:'level2', selected: ( defaultGroup === 2 ) } ); main_group.append( { type:'option', label:'Warning (3)', value:'level3', selected: ( defaultGroup === 3 ) } ); main_group.append( { type:'option', label:'Final warning (4)', value:'level4', selected: ( defaultGroup === 4 ) } ); main_group.append( { type:'option', label:'Only warning (4im)', value:'level4im', selected: ( defaultGroup === 5 ) } ); main_group.append( { type:'option', label:'Single issue notices', value:'singlenotice', selected: ( defaultGroup === 6 ) } ); main_group.append( { type:'option', label:'Single issue warnings', value:'singlewarn', selected: ( defaultGroup === 7 ) } ); if( Morebits.userIsInGroup( 'sysop' ) ) { main_group.append( { type:'option', label:'Blocking', value:'block', selected: ( defaultGroup === 8 ) } ); } main_select.append( { type:'select', name:'sub_group', event:Twinkle.warn.callback.change_subcategory } ); //Will be empty to begin with. form.append( { type:'input', name:'article', label:'Linked article', value:( Morebits.queryString.exists( 'vanarticle' ) ? Morebits.queryString.get( 'vanarticle' ) : '' ), tooltip:'An article can be linked within the notice, perhaps because it was a revert to said article that dispatched this notice. Leave empty for no article to be linked.' } ); var more = form.append( { type: 'field', name: 'reasonGroup', label: 'Warning information' } ); more.append( { type:'textarea', label:'Optional message:', name:'reason', tooltip:'Perhaps a reason, or that a more detailed notice must be appended' } ); var previewlink = document.createElement( 'a' ); $(previewlink).click(function(){ Twinkle.warn.callbacks.preview(result); // |result| is defined below }); previewlink.style.cursor = "pointer"; previewlink.textContent = 'Preview'; more.append( { type: 'div', id: 'warningpreview', label: [ previewlink ] } ); more.append( { type: 'div', id: 'twinklewarn-previewbox', style: 'display: none' } ); more.append( { type:'submit', label:'Submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); result.main_group.root = result; result.previewer = new Morebits.wiki.preview($(result).find('div#twinklewarn-previewbox').last()[0]); // We must init the first choice (General Note); var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.main_group.dispatchEvent( evt ); }; // This is all the messages that might be dispatched by the code // Each of the individual templates require the following information: // label (required): A short description displayed in the dialog // summary (required): The edit summary used. If an article name is entered, the summary is postfixed with "on [[article]]", and it is always postfixed with ". $summaryAd" // suppressArticleInSummary (optional): Set to true to suppress showing the article name in the edit summary. Useful if the warning relates to attack pages, or some such. Twinkle.warn.messages = { level1: { "uw-vandalism1": { label:"Vandalism", summary:"General note: Unhelpful changes" }, "uw-test1": { label:"Editing tests", summary:"General note: Editing tests" }, "uw-delete1": { label:"Removal of content, blanking", summary:"General note: Removal of content, blanking" }, "uw-create1": { label:"Creating inappropriate pages", summary:"General note: Creating inappropriate pages" }, "uw-advert1": { label:"Using Wikipedia for advertising or promotion", summary:"General note: Using Wikipedia for advertising or promotion" }, "uw-copyright1": { label:"Copyright violation", summary:"General note: Violating copyright" }, "uw-error1": { label:"Deliberately adding wrong information", summary:"General note: Adding wrong information" }, "uw-biog1": { label:"Adding unreferenced controversial information about living persons", summary:"General note: Adding unreferenced controversial information about living persons" }, "uw-mos1": { label:"Manual of style", summary:"General note: Formatting, date, language, etc (Manual of style)" }, "uw-move1": { label:"Page moves against naming conventions or consensus", summary:"General note: Page moves against naming conventions or consensus" }, "uw-npov1": { label:"Not adhering to neutral point of view", summary:"General note: Not adhering to neutral point of view" }, "uw-tpv1": { label:"Changing others' talk page comments", summary:"General note: Changing others' talk page comments" }, "uw-qd": { label:"Removing quick-deletion templates", summary:"General note: Removing quick-deletion templates" }, "uw-npa1": { label:"Personal attack directed at another editor", summary:"General note: Personal attack directed at another editor" }, "uw-agf1": { label:"Not assuming good faith", summary:"General note: Not assuming good faith" }, "uw-unsourced1": { label:"Addition of unsourced or improperly cited material", summary:"General note: Addition of unsourced or improperly cited material" } }, level2: { "uw-vandalism2": { label:"Vandalism", summary:"Caution: Vandalism" }, "uw-test2": { label:"Editing tests", summary:"Caution: Editing tests" }, "uw-delete2": { label:"Removal of content, blanking", summary:"Caution: Removal of content, blanking" }, "uw-create2": { label:"Creating inappropriate pages", summary:"Caution: Creating inappropriate pages" }, "uw-advert2": { label:"Using Wikipedia for advertising or promotion", summary:"Caution: Using Wikipedia for advertising or promotion" }, "uw-copyright2": { label:"Copyright violation", summary:"Caution: Violating copyright" }, "uw-npov2": { label:"Not adhering to neutral point of view", summary:"Caution: Not adhering to neutral point of view" }, "uw-error2": { label:"Deliberately adding wrong information", summary:"Caution: Adding wrong information" }, "uw-biog2": { label:"Adding unreferenced controversial information about living persons", summary:"Caution: Adding unreferenced controversial information about living persons" }, "uw-mos2": { label:"Manual of style", summary:"Caution: Formatting, date, language, etc (Manual of style)" }, "uw-move2": { label:"Page moves against naming conventions or consensus", summary:"Caution: Page moves against naming conventions or consensus" }, "uw-tpv2": { label:"Changing others' talk page comments", summary:"Caution: Changing others' talk page comments" }, "uw-npa2": { label:"Personal attack directed at another editor", summary:"Caution: Personal attack directed at another editor" }, "uw-agf2": { label:"Not assuming good faith", summary:"Caution: Not assuming good faith" }, "uw-unsourced2": { label:"Addition of unsourced or improperly cited material", summary:"Caution: Addition of unsourced or improperly cited material" } }, level3: { "uw-vandalism3": { label:"Vandalism", summary:"Warning: Vandalism" }, "uw-test3": { label:"Editing tests", summary:"Warning: Editing tests" }, "uw-delete3": { label:"Removal of content, blanking", summary:"Warning: Removal of content, blanking" }, "uw-create3": { label:"Creating inappropriate pages", summary:"Warning: Creating inappropriate pages" }, "uw-advert3": { label:"Using Wikipedia for advertising or promotion", summary:"Warning: Using Wikipedia for advertising or promotion" }, "uw-npov3": { label:"Not adhering to neutral point of view", summary:"Warning: Not adhering to neutral point of view" }, "uw-error3": { label:"Deliberately adding wrong information", summary:"Warning: Adding wrong information" }, "uw-biog3": { label:"Adding unreferenced controversial or defamatory information about living persons", summary:"Warning: Adding unreferenced controversial information about living persons" }, "uw-mos3": { label:"Manual of style", summary:"Warning: Formatting, date, language, etc (Manual of style)" }, "uw-move3": { label:"Page moves against naming conventions or consensus", summary:"Warning: Page moves against naming conventions or consensus" }, "uw-tpv3": { label:"Changing others' talk page comments", summary:"Warning: Changing others' talk page comments" }, "uw-npa3": { label:"Personal attack directed at another editor", summary:"Warning: Personal attack directed at another editor" }, "uw-agf3": { label:"Not assuming good faith", summary:"Warning: Not assuming good faith" } }, level4: { "uw-generic4": { label:"Generic warning (for template series missing level 4)", summary:"Final warning notice" }, "uw-vandalism4": { label:"Vandalism", summary:"Final warning: Vandalism" }, "uw-test4": { label:"Editing tests", summary:"Final warning: Editing tests" }, "uw-delete4": { label:"Removal of content, blanking", summary:"Final warning: Removal of content, blanking" }, "uw-create4": { label:"Creating inappropriate pages", summary:"Final warning: Creating inappropriate pages" }, "uw-advert4": { label:"Using Wikipedia for advertising or promotion", summary:"Final warning: Using Wikipedia for advertising or promotion" }, "uw-npov4": { label:"Not adhering to neutral point of view", summary:"Final warning: Not adhering to neutral point of view" }, "uw-error4": { label:"Deliberately adding wrong information", summary:"Final Warning: Adding wrong information" }, "uw-biog4": { label:"Adding unreferenced defamatory information about living persons", summary:"Final warning: Adding unreferenced controversial information about living persons" }, "uw-mos4": { label:"Manual of style", summary:"Final warning: Formatting, date, language, etc (Manual of style)" }, "uw-move4": { label:"Page moves against naming conventions or consensus", summary:"Final warning: Page moves against naming conventions or consensus" }, "uw-npa4": { label:"Personal attack directed at another editor", summary:"Final warning: Personal attack directed at another editor" } }, level4im: { "uw-vandalism4im": { label:"Vandalism", summary:"Only warning: Vandalism" }, "uw-delete4im": { label:"Removal of content, blanking", summary:"Only warning: Removal of content, blanking" }, "uw-create4im": { label:"Creating inappropriate pages", summary:"Only warning: Creating inappropriate pages" }, "uw-biog4im": { label:"Adding unreferenced defamatory information about living persons", summary:"Only warning: Adding unreferenced controversial information about living persons" }, "uw-move4im": { label:"Page moves against naming conventions or consensus", summary:"Only warning: Page moves against naming conventions or consensus" }, "uw-npa4im": { label:"Personal attack directed at another editor", summary:"Only warning: Personal attack directed at another editor" } }, singlenotice: { "uw-badcat": { label:"Adding incorrect categories", summary:"Notice: Adding incorrect categories" }, "uw-bite": { label:"\"Biting\" newcomers", summary:"Notice: \"Biting\" newcomers" }, "uw-coi": { label:"Possible conflict of interest", summary:"Notice: Possible conflict of interest" }, "uw-encopypaste": { label:"Direct copying of article from English Wikipedia", summary:"Notice: Direct copying of article from English Wikipedia" }, "uw-encopyright": { label:"Not giving attribution for content from another Wikipedia", summary:"Notice: Reusing content from English Wikipedia without attribution" }, "uw-emptycat": { label:"Category created does not contain enough pages", summary:"Notice: Creating empty categories" }, "uw-joke": { label:"Using improper humor", summary:"Notice: Using improper humor" }, "uw-lang": { label:"Changing between types of English without a good reason", summary:"Notice: Unnecessarily changing between British and American English" }, "uw-newarticle": { label:"Tips on creating new articles", summary:"Notice: How to make your articles better" }, "uw-notenglish": { label:"Changes not in English", summary:"Notice: Please edit in English" }, "uw-otherweb": { label:"Use \"Other websites\", not \"External links\"", summary:"Notice: Use \"Other websites\", not \"External links\"" }, "uw-sandbox": { label:"Removing the sandbox header", summary:"Notice: Do not remove sandbox header" }, "uw-selfrevert": { label:"Undoing recent test", summary:"Notice: Undoing recent test" }, "uw-simple": { label:"Not making changes in simple English", summary:"Notice: Not making changes in simple English" }, "uw-spellcheck": { label:"Review spelling, etc.", summary:"Notice: Review spelling, etc." }, "uw-subst": { label:"Remember to subst: templates", summary:"Notice: Remember to subst: templates" }, "uw-tilde": { label:"Not signing posts", summary:"Notice: Not signing posts" }, "uw-upload": { label:"Image uploads not allowed in Simple English Wikipedia", summary:"Notice: Image uploads not allowed in Simple English Wikipedia" }, "uw-warn": { label:"Use user warn templates", summary:"Notice: Use user warn templates" } }, singlewarn: { "uw-3rr": { label:"Edit warring", summary:"Warning: Involved in edit war" }, "uw-attack": { label:"Creating attack pages", summary:"Warning: Creating attack pages" }, "uw-cyberbully": { label:"Cyberbullying", summary:"Warning: Cyberbullying" }, "uw-disruption": { label:"Project disruption", summary:"Warning: Project disruption" }, "uw-longterm": { label:"Long term abuse", summary:"Warning: Long term abuse" }, "uw-qd": { label:"Removing quick deletion templates from articles", summary:"Warning: Removing quick deletion templates from articles" }, "uw-spam": { label:"Adding spam links", summary:"Warning: Adding spam links" }, "uw-userpage": { label:"Userpage or subpage is against policy", summary:"Warning: Userpage or subpage is against policy" } }, block: { "uw-block1": { label: "Block level 1", summary: "You have been temporarily blocked", reasonParam: true }, "uw-block2": { label: "Block level 2", summary: "You have been blocked", reasonParam: true }, "uw-block3": { label: "Block level 3", summary: "You have been indefinitely blocked", reasonParam: true }, "UsernameBlocked": { label: "Username block", summary: "You have been blocked for violation of the [[Wikipedia:Username|username policy]]", reasonParam: true }, "UsernameHardBlocked": { label: "Username hard block", summary: "You have been blocked for a blatant violation of the [[Wikipedia:Username|username policy]]", reasonParam: true }, "Blocked proxy": { label: "Blocked proxy", summary: "You have been blocked because this IP is an [[open proxy]]" }, "Uw-spamblock": { label: "Spam block", summary: "You have been blocked for [[Wikipedia:Spam|advertising or promotion]]" }, "Cyberbully block": { label: "Cyberbully block", summary: "You have been blocked for [[Wikipedia:Cyberbullying|cyberbullying]]" }, "Talkpage-revoked": { label: "Talk-page access removed", summary: "Your ability to change this [[Wikipedia:Talk page|talk page]] has been removed" } } }; Twinkle.warn.prev_block_timer = null; Twinkle.warn.prev_block_reason = null; Twinkle.warn.prev_article = null; Twinkle.warn.prev_reason = null; Twinkle.warn.callback.change_category = function twinklewarnCallbackChangeCategory(e) { var value = e.target.value; var sub_group = e.target.root.sub_group; var messages = Twinkle.warn.messages[ value ]; sub_group.main_group = value; var old_subvalue = sub_group.value; var old_subvalue_re; if( old_subvalue ) { old_subvalue = old_subvalue.replace(/\d*(im)?$/, '' ); old_subvalue_re = new RegExp( RegExp.escape( old_subvalue ) + "(\\d*(?:im)?)$" ); } while( sub_group.hasChildNodes() ){ sub_group.removeChild( sub_group.firstChild ); } for( var i in messages ) { var selected = false; if( old_subvalue && old_subvalue_re.test( i ) ) { selected = true; } var elem = new Morebits.quickForm.element( { type:'option', label:"{{" + i + "}}: " + messages[i].label, value:i, selected: selected } ); sub_group.appendChild( elem.render() ); } if( value === 'block' ) { // create the block-related fields var more = new Morebits.quickForm.element( { type: 'div', id: 'block_fields' } ); more.append( { type: 'input', name: 'block_timer', label: 'Period of blocking / Host ', tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc... If you selected "blocked proxy", this text box will append the host name of the server' } ); more.append( { type: 'input', name: 'block_reason', label: '"You have been blocked for ..." ', tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.' } ); e.target.root.insertBefore( more.render(), e.target.root.lastChild ); // restore saved values of fields if(Twinkle.warn.prev_block_timer !== null) { e.target.root.block_timer.value = Twinkle.warn.prev_block_timer; Twinkle.warn.prev_block_timer = null; } if(Twinkle.warn.prev_block_reason !== null) { e.target.root.block_reason.value = Twinkle.warn.prev_block_reason; Twinkle.warn.prev_block_reason = null; } if(Twinkle.warn.prev_article === null) { Twinkle.warn.prev_article = e.target.root.article.value; } e.target.root.article.disabled = false; $(e.target.root.reason).parent().hide(); e.target.root.previewer.closePreview(); } else if( e.target.root.block_timer ) { // hide the block-related fields if(!e.target.root.block_timer.disabled && Twinkle.warn.prev_block_timer === null) { Twinkle.warn.prev_block_timer = e.target.root.block_timer.value; } if(!e.target.root.block_reason.disabled && Twinkle.warn.prev_block_reason === null) { Twinkle.warn.prev_block_reason = e.target.root.block_reason.value; } $(e.target.root).find("#block_fields").remove(); if(e.target.root.article.disabled && Twinkle.warn.prev_article !== null) { e.target.root.article.value = Twinkle.warn.prev_article; Twinkle.warn.prev_article = null; } e.target.root.article.disabled = false; $(e.target.root.reason).parent().show(); e.target.root.previewer.closePreview(); } // clear overridden label on article textbox Morebits.quickForm.setElementTooltipVisibility(e.target.root.article, true); Morebits.quickForm.resetElementLabel(e.target.root.article); }; Twinkle.warn.callback.change_subcategory = function twinklewarnCallbackChangeSubcategory(e) { var main_group = e.target.form.main_group.value; var value = e.target.form.sub_group.value; if( main_group === 'singlewarn' ) { if( value === 'uw-username' ) { if(Twinkle.warn.prev_article === null) { Twinkle.warn.prev_article = e.target.form.article.value; } e.target.form.article.notArticle = true; e.target.form.article.value = ''; } else if( e.target.form.article.notArticle ) { if(Twinkle.warn.prev_article !== null) { e.target.form.article.value = Twinkle.warn.prev_article; Twinkle.warn.prev_article = null; } e.target.form.article.notArticle = false; } } else if( main_group === 'block' ) { if( Twinkle.warn.messages.block[value].indefinite ) { if(Twinkle.warn.prev_block_timer === null) { Twinkle.warn.prev_block_timer = e.target.form.block_timer.value; } e.target.form.block_timer.disabled = true; e.target.form.block_timer.value = 'indefinite'; } else if( e.target.form.block_timer.disabled ) { if(Twinkle.warn.prev_block_timer !== null) { e.target.form.block_timer.value = Twinkle.warn.prev_block_timer; Twinkle.warn.prev_block_timer = null; } e.target.form.block_timer.disabled = false; } if( Twinkle.warn.messages.block[value].pageParam ) { if(Twinkle.warn.prev_article !== null) { e.target.form.article.value = Twinkle.warn.prev_article; Twinkle.warn.prev_article = null; } e.target.form.article.disabled = false; } else if( !e.target.form.article.disabled ) { if(Twinkle.warn.prev_article === null) { Twinkle.warn.prev_article = e.target.form.article.value; } e.target.form.article.disabled = true; e.target.form.article.value = ''; } if( Twinkle.warn.messages.block[value].reasonParam ) { if(Twinkle.warn.prev_block_reason !== null) { e.target.form.block_reason.value = Twinkle.warn.prev_block_reason; Twinkle.warn.prev_block_reason = null; } e.target.form.block_reason.disabled = false; } else if( !e.target.form.block_reason.disabled ) { if(Twinkle.warn.prev_block_reason === null) { Twinkle.warn.prev_block_reason = e.target.form.block_reason.value; } e.target.form.block_reason.disabled = true; e.target.form.block_reason.value = ''; } } // change form labels according to the warning selected if (value === "uw-username") { Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, false); Morebits.quickForm.overrideElementLabel(e.target.form.article, "Username violates policy because... "); } else { Morebits.quickForm.setElementTooltipVisibility(e.target.form.article, true); Morebits.quickForm.resetElementLabel(e.target.form.article); } }; Twinkle.warn.callbacks = { preview: function(form) { var templatename = form.sub_group.value; var templatetext = '{{subst:' + templatename; var linkedarticle = form.article.value; if (templatename in Twinkle.warn.messages.block) { if( linkedarticle && Twinkle.warn.messages.block[templatename].pageParam ) { templatetext += '|page=' + linkedarticle; } var blocktime = form.block_timer.value; if( /te?mp|^\s*$|min/.exec( blocktime ) || Twinkle.warn.messages.block[templatename].indefinite ) { ; // nothing } else if( /indef|\*|max/.exec( blocktime ) ) { templatetext += '|indef=yes'; } else { templatetext += '|host=' + blocktime; templatetext += '|time=' + blocktime; } var blockreason = form.block_reason.value; if( blockreason ) { templatetext += '|reason=' + blockreason; } templatetext += "|sig=true}}"; } else { if (linkedarticle) { // add linked article for user warnings (non-block templates) templatetext += '|1=' + linkedarticle; } templatetext += '}}'; // add extra message for non-block templates var reason = form.reason.value; if (reason) { templatetext += " ''" + reason + "''"; } } form.previewer.beginRender(templatetext); }, main: function( pageobj ) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var messageData = Twinkle.warn.messages[params.main_group][params.sub_group]; var history_re = /<!-- Template:(uw-.*?) -->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4}) \(UTC\)/g; var history = {}; var latest = { date:new Date( 0 ), type:'' }; var current; while( ( current = history_re.exec( text ) ) ) { var current_date = new Date( current[2] + ' UTC' ); if( !( current[1] in history ) || history[ current[1] ] < current_date ) { history[ current[1] ] = current_date; } if( current_date > latest.date ) { latest.date = current_date; latest.type = current[1]; } } var date = new Date(); if( params.sub_group in history ) { var temp_time = new Date( history[ params.sub_group ] ); temp_time.setUTCHours( temp_time.getUTCHours() + 24 ); if( temp_time > date ) { if( !confirm( "An identical " + params.sub_group + " has been issued in the last 24 hours. \nWould you still like to add this warning/notice?" ) ) { pageobj.statelem.info( 'aborted per user request' ); return; } } } latest.date.setUTCMinutes( latest.date.getUTCMinutes() + 1 ); // after long debate, one minute is max if( latest.date > date ) { if( !confirm( "A " + latest.type + " has been issued in the last minute. \nWould you still like to add this warning/notice?" ) ) { pageobj.statelem.info( 'aborted per user request' ); return; } } var mainheaderRe = new RegExp("==+\\s*Warnings\\s*==+"); var headerRe = new RegExp( "^==+\\s*(?:" + date.getUTCMonthName() + '|' + date.getUTCMonthNameAbbrev() + ")\\s+" + date.getUTCFullYear() + "\\s*==+", 'm' ); if( text.length > 0 ) { text += "\n\n"; } if( params.main_group === 'block' ) { var article = '', reason = '', host = '', time = null; if( Twinkle.getPref('blankTalkpageOnIndefBlock') && params.sub_group !== 'uw-lblock' && ( Twinkle.warn.messages.block[params.sub_group].indefinite || (/indef|\*|max/).exec( params.block_timer ) ) ) { Morebits.status.info( 'Info', 'Blanking talk page per preferences and creating a new level 2 heading for the date' ); text = "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n"; } else if( !headerRe.exec( text ) ) { Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' ); text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n"; } if( params.reason && Twinkle.warn.messages.block[params.sub_group].reasonParam ) { reason = '|reason=' + params.reason; } if( /te?mp|^\s*$|min/.exec( params.block_timer ) || Twinkle.warn.messages.block[params.sub_group].indefinite ) { time = ''; } else if( /indef|\*|max/.exec( params.block_timer ) ) { time = '|indef=yes'; } else { time = '|time=' + params.block_timer; } if ( params.sub_group === "Blocked proxy" ) { text += "{{" + params.sub_group + "|host=" + params.block_timer + "}}"; } else { text += "{{subst:" + params.sub_group + time + reason + "|sig=yes}}"; } } else { if( !headerRe.exec( text ) ) { Morebits.status.info( 'Info', 'Will create a new level 2 heading for the date, as none was found for this month' ); text += "== " + date.getUTCMonthName() + " " + date.getUTCFullYear() + " ==\n"; } text += "{{subst:" + params.sub_group + ( params.article ? '|1=' + params.article : '' ) + "|subst=subst:}}" + (params.reason ? " ''" + params.reason + "'' ": ' ' ) + "~~~~"; } if ( Twinkle.getPref('showSharedIPNotice') && Morebits.isIPAddress( mw.config.get('wgTitle') ) ) { Morebits.status.info( 'Info', 'Adding a shared IP notice' ); text += "\n{{subst:SharedIPAdvice}}"; } var summary = messageData.summary; if ( messageData.suppressArticleInSummary !== true && params.article ) { summary += " on [[" + params.article + "]]"; } summary += "." + Twinkle.getPref("summaryAd"); pageobj.setPageText( text ); pageobj.setEditSummary( summary ); pageobj.setWatchlist( Twinkle.getPref('watchWarnings') ); pageobj.save(); } }; Twinkle.warn.callback.evaluate = function twinklewarnCallbackEvaluate(e) { // First, check to make sure a reason was filled in if uw-username was selected if(e.target.sub_group.value === 'uw-username' && e.target.article.value.trim() === '') { alert("You must supply a reason for the {{uw-username}} template."); return; } // Then, grab all the values provided by the form var params = { reason: e.target.block_reason ? e.target.block_reason.value : e.target.reason.value, main_group: e.target.main_group.value, sub_group: e.target.sub_group.value, article: e.target.article.value, // .replace( /^(Image|Category):/i, ':$1:' ), -- apparently no longer needed... block_timer: e.target.block_timer ? e.target.block_timer.value : null }; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName'); Morebits.wiki.actionCompleted.notice = "Warning complete, reloading talk page in a few seconds"; var wikipedia_page = new Morebits.wiki.page( mw.config.get('wgPageName'), 'User talk page modification' ); wikipedia_page.setCallbackParameters( params ); wikipedia_page.setFollowRedirect( true ); wikipedia_page.load( Twinkle.warn.callbacks.main ); }; /* **************************************** *** twinklexfd.js: XFD module **************************************** * Mode of invocation: Tab ("XFD") * Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects * Config directives in: TwinkleConfig */ Twinkle.xfd = function twinklexfd() { // Disable on: // * special pages // * non-existent pages // * files on Commons, whether there is a local page or not (unneeded local pages of files on Commons are eligible for CSD F2) // * file pages without actual files (these are eligible for CSD G8) if ( mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') || (mw.config.get('wgNamespaceNumber') === 6 && (document.getElementById('mw-sharedupload') || (!document.getElementById('mw-imagepage-section-filehistory') && !Morebits.wiki.isPageRedirect()))) ) { return; } //twAddPortletLink( Twinkle.xfd.callback, "RfD", "tw-xfd", "Nominate for deletion" ); }; Twinkle.xfd.num2order = function twinklexfdNum2order( num ) { switch( num ) { case 1: return ''; case 2: return '2nd'; case 3: return '3rd'; default: return num + 'th'; } }; Twinkle.xfd.currentRationale = null; // error callback on Morebits.status.object Twinkle.xfd.printRationale = function twinklexfdPrintRationale() { if (Twinkle.xfd.currentRationale) { var p = document.createElement("p"); p.textContent = "Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:"; var pre = document.createElement("pre"); pre.className = "toccolours"; pre.style.marginTop = "0"; pre.textContent = Twinkle.xfd.currentRationale; p.appendChild(pre); Morebits.status.root.appendChild(p); // only need to print the rationale once Twinkle.xfd.currentRationale = null; } }; Twinkle.xfd.callback = function twinklexfdCallback() { if (!twinkleUserAuthorized) { alert("Your account is too new to use Twinkle."); return; } var Window = new Morebits.simpleWindow( 600, 350 ); Window.setTitle( "Nominate for deletion (RfD)" ); Window.setScriptName( "Twinkle" ); Window.addFooterLink( "Deletion policy", "Wikipedia:Deletion policy" ); Window.addFooterLink( "About deletion discussions", "WP:RfD" ); Window.addFooterLink( "Twinkle help", "mh:dev:Twinkle/Documentation#xfd" ); var form = new Morebits.quickForm( Twinkle.xfd.callback.evaluate ); var categories = form.append( { type: 'select', name: 'category', label: 'Select wanted type of category: ', tooltip: 'This default should be the most appropriate, as no other deletion discussion pages exist here.', event: Twinkle.xfd.callback.change_category } ); categories.append( { type: 'option', label: 'RfD (Requests for deletion)', selected: true, value: 'afd' } ); form.append( { type: 'checkbox', list: [ { label: 'Notify page creator if possible', value: 'notify', name: 'notify', tooltip: "A notification template will be placed on the creator's talk page if this is true.", checked: true } ] } ); form.append( { type: 'field', label:'Work area', name: 'work_area' } ); form.append( { type:'submit' } ); var result = form.render(); Window.setContent( result ); Window.display(); // We must init the controls var evt = document.createEvent( "Event" ); evt.initEvent( 'change', true, true ); result.category.dispatchEvent( evt ); }; Twinkle.xfd.previousNotify = true; Twinkle.xfd.callback.change_category = function twinklexfdCallbackChangeCategory(e) { var value = e.target.value; var form = e.target.form; var old_area = Morebits.quickForm.getElements(e.target.form, "work_area")[0]; var work_area = null; var oldreasontextbox = form.getElementsByTagName('textarea')[0]; var oldreason = (oldreasontextbox ? oldreasontextbox.value : ''); work_area = new Morebits.quickForm.element( { type: 'field', label: 'Requests for deletion', name: 'work_area' } ); work_area.append( { type: 'checkbox', list: [ { label: 'Wrap deletion tag with <noinclude>', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; tags, so that it won\'t transclude. This option is not normally required.' } ] } ); work_area.append( { type: 'textarea', name: 'xfdreason', label: 'Reason: ' } ); work_area = work_area.render(); old_area.parentNode.replaceChild( work_area, old_area ); } Twinkle.xfd.callbacks = { afd: { main: function(apiobj) { var xmlDoc = apiobj.responseXML; var titles = $(xmlDoc).find('allpages p'); // There has been no earlier entries with this prefix, just go on. if( titles.length <= 0 ) { apiobj.params.numbering = apiobj.params.number = ''; } else { var number = 0; for( var i = 0; i < titles.length; ++i ) { var title = titles[i].getAttribute('title'); // First, simple test, is there an instance with this exact name? if( title === 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') ) { number = Math.max( number, 1 ); continue; } var order_re = new RegExp( '^' + RegExp.escape( 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'), true ) + '\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$'); var match = order_re.exec( title ); // No match; A non-good value if( !match ) { continue; } // A match, set number to the max of current number = Math.max( number, Number(match[1]) ); } apiobj.params.number = Twinkle.xfd.num2order( parseInt( number, 10 ) + 1); apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : ''; } apiobj.params.discussionpage = 'Wikipedia:Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + apiobj.params.numbering; Morebits.status.info( "Next discussion page", "[[" + apiobj.params.discussionpage + "]]" ); // Updating data for the action completed event Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage; Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to the discussion page"; // Tagging article var wikipedia_page = new Morebits.wiki.page(mw.config.get('wgPageName'), "Adding deletion tag to article"); if(window.location.search.includes("redirect=no")) { wikipedia_page.setFollowRedirect(false); // User's intention was probably to tag the redirect itself } else { wikipedia_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect } wikipedia_page.setCallbackParameters(apiobj.params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle); }, // Tagging needs to happen before everything else: this means we can check if there is an AfD tag already on the page taggingArticle: function(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); var statelem = pageobj.getStatusElement(); // Check for existing AfD tag, for the benefit of new page patrollers var textNoAfd = text.replace(/\{\{\s*(Requests for deletion\/dated|RfDM)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, ""); if (text !== textNoAfd) { if (confirm("An RfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current RfD tag (not recommended), or Cancel to abandon your nomination.")) { text = textNoAfd; } else { statelem.error("Article already tagged with RfD tag, and you chose to abort"); window.location.reload(); return; } } // Now we know we want to go ahead with it, trigger the other AJAX requests // Starting discussion page var wikipedia_page = new Morebits.wiki.page(params.discussionpage, "Creating article deletion discussion page"); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage); // Today's list var date = new Date(); wikipedia_page = new Morebits.wiki.page('Wikipedia:Requests for deletion', "Adding discussion to today's list"); wikipedia_page.setFollowRedirect(true); wikipedia_page.setCallbackParameters(params); wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList); // Notification to first contributor if (params.usertalk) { var thispage = new Morebits.wiki.page(mw.config.get('wgPageName')); thispage.setCallbackParameters(params); thispage.lookupCreator(Twinkle.xfd.callbacks.afd.userNotification); } // Then, test if there are speedy deletion-related templates on the article. var textNoSd = text.replace(/\{\{\s*(db(-\w*)?|qd|delete|(?:hang|hold)[\- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, ""); if (text !== textNoSd && confirm("A quick deletion tag was found on this page. Should it be removed?")) { text = textNoSd; } pageobj.setPageText(( params.noinclude ? "<noinclude>" : "" ) + "\{\{RfD|" + params.reason + "\}\}\n" + ( params.noinclude ? "</noinclude>" : "" ) + text); pageobj.setEditSummary("Nominated for deletion; see [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd')); switch (Twinkle.getPref('xfdWatchPage')) { case 'yes': pageobj.setWatchlist(true); break; case 'no': pageobj.setWatchlistFromPreferences(false); break; default: pageobj.setWatchlistFromPreferences(true); break; } pageobj.setCreateOption('nocreate'); pageobj.save(); }, discussionPage: function(pageobj) { var text = pageobj.getPageText(); var params = pageobj.getCallbackParameters(); pageobj.setPageText("{{subst:RfD/Preload/Template|deletereason=" + params.reason + "}}\n"); pageobj.setEditSummary("Creating deletion discussion page for [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); switch (Twinkle.getPref('xfdWatchDiscussion')) { case 'yes': pageobj.setWatchlist(true); break; case 'no': pageobj.setWatchlistFromPreferences(false); break; default: pageobj.setWatchlistFromPreferences(true); break; } pageobj.setCreateOption('createonly'); pageobj.save(function() { Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki }); }, todaysList: function(pageobj) { var old_text = pageobj.getPageText() + "\n"; // MW strips trailing blanks, but we like them, so we add a fake one var params = pageobj.getCallbackParameters(); var statelem = pageobj.getStatusElement(); var text = old_text.replace( /(<\!-- Add new entries to the TOP of the following list -->\n+)/, "$1{{Wikipedia:Requests for deletion/Requests/" + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName') + params.numbering + "}}\n"); if( text === old_text ) { statelem.error( 'failed to find target spot for the discussion' ); return; } pageobj.setPageText(text); pageobj.setEditSummary("Adding [[" + params.discussionpage + "]]." + Twinkle.getPref('summaryAd')); switch (Twinkle.getPref('xfdWatchList')) { case 'yes': pageobj.setWatchlist(true); break; case 'no': pageobj.setWatchlistFromPreferences(false); break; default: pageobj.setWatchlistFromPreferences(true); break; } pageobj.setCreateOption('recreate'); pageobj.save(); }, userNotification: function(pageobj) { var params = pageobj.getCallbackParameters(); var initialContrib = pageobj.getCreator(); var usertalkpage = new Morebits.wiki.page('User talk:' + initialContrib, "Notifying initial contributor (" + initialContrib + ")"); var notifytext = "\n{{subst:RFDNote|1=" + mw.config.get('wgPageName') + "|2=" + mw.config.get('wgPageName') + ( params.numbering !== '' ? '|order=&#32;' + params.numbering : '' ) + "}} ~~~~"; usertalkpage.setAppendText(notifytext); usertalkpage.setEditSummary("Notification: listing at [[WP:RfD|requests for deletion]] of [[" + mw.config.get('wgPageName') + "]]." + Twinkle.getPref('summaryAd')); usertalkpage.setCreateOption('recreate'); switch (Twinkle.getPref('xfdWatchUser')) { case 'yes': usertalkpage.setWatchlist(true); break; case 'no': usertalkpage.setWatchlistFromPreferences(false); break; default: usertalkpage.setWatchlistFromPreferences(true); break; } usertalkpage.setFollowRedirect(true); usertalkpage.append(); } } }; Twinkle.xfd.callback.evaluate = function(e) { mw.config.set('wgPageName', mw.config.get('wgPageName').replace(/_/g, ' ')); // for queen/king/whatever and country! var type = e.target.category.value; var usertalk = e.target.notify.checked; var reason = e.target.xfdreason.value; var xfdtarget, xfdtarget2, puf, noinclude, tfdinline, notifyuserspace; Morebits.simpleWindow.setButtonsEnabled( false ); Morebits.status.init( e.target ); Twinkle.xfd.currentRationale = reason; Morebits.status.onError(Twinkle.xfd.printRationale); if( !type ) { Morebits.status.error( 'Error', 'no action given' ); return; } var query, wikipedia_page, wikipedia_api, logpage, params; var date = new Date(); query = { 'action': 'query', 'list': 'allpages', 'apprefix': 'Requests for deletion/Requests/' + ((new Date()).getUTCFullYear()) + '/' + mw.config.get('wgPageName'), 'apnamespace': 4, 'apfilterredir': 'nonredirects', 'aplimit': Morebits.userIsInGroup( 'sysop' ) ? 5000 : 500 }; wikipedia_api = new Morebits.wiki.api( 'Tagging article with deletion tag', query, Twinkle.xfd.callbacks.afd.main ); wikipedia_api.params = { usertalk:usertalk, reason:reason, noinclude:noinclude }; wikipedia_api.post(); }; /** * General initialization code */ var scriptpathbefore = "https://dev.miraheze.org" + mw.util.wikiScript( "index" ) + "?title=", scriptpathafter = "&action=raw&ctype=text/javascript&happy=yes"; // Retrieve the user's Twinkle preferences $.ajax({ url: scriptpathbefore + "User:" + encodeURIComponent( mw.config.get("wgUserName")) + "/twinkleoptions.js" + scriptpathafter, dataType: "text", error: function () { mw.notify( "Could not load twinkleoptions.js" ); }, success: function ( optionsText ) { // Quick pass if user has no options if ( optionsText === "" ) { return; } // Twinkle options are basically a JSON object with some comments. Strip those: optionsText = optionsText.replace( /(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, "" ); // First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line. if ( optionsText.lastIndexOf( "window.Twinkle.prefs = ", 0 ) === 0 ) { optionsText = optionsText.replace( /(?:^window.Twinkle.prefs = |;\n*$)/g, "" ); } try { var options = JSON.parse( optionsText ); // Assuming that our options evolve, we will want to transform older versions: //if ( options.optionsVersion === undefined ) { // ... // options.optionsVersion = 1; //} //if ( options.optionsVersion === 1 ) { // ... // options.optionsVersion = 2; //} // At the same time, twinkleconfig.js needs to be adapted to write a higher version number into the options. if ( options ) { Twinkle.prefs = options; } } catch ( e ) { mw.notify("Could not parse twinkleoptions.js"); } }, complete: function () { $( Twinkle.load ); } }); // Developers: you can import custom Twinkle modules here // For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter); Twinkle.load = function () { // Don't activate on special pages other than "Contributions" so that they load faster, especially the watchlist. // Also, Twinkle is incompatible with Internet Explorer versions 8 or lower, so don't load there either. var specialPageWhitelist = [ 'Block', 'Contributions', 'Recentchanges', 'Recentchangeslinked' ]; // wgRelevantUserName defined for non-sysops on Special:Block if (Morebits.userIsInGroup('sysop')) { specialPageWhitelist = specialPageWhitelist.concat([ 'DeletedContributions', 'Prefixindex' ]); } if (mw.config.get('wgNamespaceNumber') === -1 && specialPageWhitelist.indexOf(mw.config.get('wgCanonicalSpecialPageName')) === -1) { return; } // Prevent clickjacking if (window.top !== window.self) { return; } if ($.client.profile().name === 'msie' && $.client.profile().versionNumber < 9) { return; } // Set custom Api-User-Agent header, for server-side logging purposes Morebits.wiki.api.setApiUserAgent('Twinkle/2.0 (' + mw.config.get('wgDBname') + ')'); // Load the modules in the order that the tabs should appears // Deletion Twinkle.speedy(); // Misc. ones last Twinkle.diff(); Twinkle.unlink(); Twinkle.config.init(); Twinkle.fluff.init(); if ( Morebits.userIsInGroup('sysop') ) { Twinkle.batchdelete(); Twinkle.batchprotect(); Twinkle.batchundelete(); } // Run the initialization callbacks for any custom modules $( Twinkle.initCallbacks ).each(function ( k, v ) { v(); }); Twinkle.addInitCallback = function ( func ) { func(); }; // Increases text size in Twinkle dialogs, if so configured if ( Twinkle.getPref( "dialogLargeFont" ) ) { mw.util.addCSS( ".morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } " + ".morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }" ); } }; } ( window, document, jQuery )); // End wrap with anonymous function // </nowiki> 3399804aac47c3b253525d584e1077068e37daf4 MediaWiki:Twinkle.js 8 84 161 160 2023-08-28T02:15:26Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 javascript text/javascript /* twinkle js file [[Category:Twinkle]] loads all dependencies then twinkle */ mw.loader.getScript( 'https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.js?action=raw&ctype=text/javascript' ).then( function () {mw.loader.load(["mediawiki.util"]);mw.loader.load(["jquery.ui"]);mw.loader.load(["jquery.tipsy"]);mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-morebits.css?action=raw&ctype=text/css", "text/css");mw.loader.load("https://dev.miraheze.org/wiki/MediaWiki:Gadget-Twinkle.js?action=raw&ctype=text/javascript");}, function ( e ) {mw.log.error( e.message );} ); a49ff2e963c82cee4e45c90883102d1e662bd8b8 Template:Uses TemplateStyles 10 85 163 162 2023-08-28T02:15:26Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 wikitext text/x-wiki <includeonly>{{#invoke:Uses TemplateStyles|main}}</includeonly><noinclude> {{Uses TemplateStyles|Template:Uses TemplateStyles/example.css|nocat=true}} {{documentation}} <!-- Categories go on the /doc subpage and interwikis go on Wikidata. --> </noinclude> 7e26d8f257e302bd8a3dcbe53f52741ae0884f74 Module:Uses TemplateStyles 828 86 165 164 2023-08-28T02:15:27Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain -- This module implements the {{Uses TemplateStyles}} template. local mMessageBox = require('Module:Message box') local p = {} function p.main(frame) local origArgs = frame:getParent().args local args = {} for k, v in pairs(origArgs) do v = v:match('^%s*(.-)%s*$') if v ~= '' then args[k] = v end end return p._main(args) end function p._main(args) return p.renderBox(args) end function p.renderBox(tStyles) local boxArgs = {} if #tStyles < 1 then boxArgs.text = '<strong class="error">Error: no TemplateStyles specified</strong>' else local tStylesLinks = {} for i, ts in ipairs(tStyles) do local sandboxLink = nil local tsTitle = mw.title.new(ts) if tsTitle then local tsSandboxTitle = mw.title.new(string.format('%s:%s/sandbox/%s', tsTitle.nsText, tsTitle.baseText, tsTitle.subpageText)) if tsSandboxTitle and tsSandboxTitle.exists then sandboxLink = string.format(' ([[:%s|sandbox]])', tsSandboxTitle.prefixedText) end end tStylesLinks[i] = string.format('[[:%s]]%s', ts, sandboxLink or '') end local tStylesList = mw.text.listToText(tStylesLinks) boxArgs.text = 'This ' .. (mw.title.getCurrentTitle():inNamespaces(828,829) and 'module' or 'template') .. ' uses [[mw:Extension:TemplateStyles|TemplateStyles]]:\n' .. tStylesList end boxArgs.type = 'notice' boxArgs.small = true boxArgs.image = '[[File:Farm-Fresh css add.svg|32px|alt=CSS]]' return mMessageBox.main('mbox', boxArgs) end return p 3c7364ddaba9beb17a73b0f5256cd7fc3b3051f4 Module:No globals 828 87 167 166 2023-08-28T02:15:27Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain local mt = getmetatable(_G) or {} function mt.__index (t, k) if k ~= 'arg' then error('Tried to read nil global ' .. tostring(k), 2) end return nil end function mt.__newindex(t, k, v) if k ~= 'arg' then error('Tried to write global ' .. tostring(k), 2) end rawset(t, k, v) end setmetatable(_G, mt) 8ce3969f7d53b08bd00dabe4cc9780bc6afd412a Module:Reply to 828 88 169 168 2023-08-28T02:15:28Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain local p = {} local function makeError(msg) msg ='Error in [[Template:Reply to]]: ' .. msg return mw.text.tag('strong', {['class']='error'}, msg) end function p.replyto(frame) local origArgs = frame:getParent().args local args = {} local maxArg = 1 local usernames = 0 for k, v in pairs(origArgs) do if type(k) == 'number' then if mw.ustring.match(v,'%S') then if k > maxArg then maxArg = k end usernames = usernames + 1 local title = mw.title.new(v) if not title then return makeError('Input contains forbidden characters.') end args[k] = title.rootText end elseif v == '' and k:sub(0,5) == 'label' then args[k] = '&#x200B;' else args[k] = v end end if usernames > (tonumber(frame.args.max) or 50) then return makeError(string.format( 'More than %s names specified.', tostring(frame.args.max or 50) )) else if usernames < 1 then if frame.args.example then args[1] = frame.args.example else return makeError('Username not given.') end end args['label1'] = args['label1'] or args['label'] local isfirst = true local outStr = args['prefix'] or '@' for i = 1, maxArg do if args[i] then if isfirst then isfirst = false else if ( (usernames > 2) or ((usernames == 2) and (args['c'] == '')) ) then outStr = outStr..', ' end if i == maxArg then outStr = outStr..' '..(args['c'] or 'and') .. ' ' end end outStr = string.format( '%s[[User:%s|%s]]', outStr, args[i], args['label'..tostring(i)] or args[i] ) end end outStr = outStr..(args['p'] or ':') return mw.text.tag('span', {['class']='template-ping'}, outStr) end end return p 14f0cd73a8a9f122c0e0e15382219083c602c62a Module:Formatted appearance 828 89 171 170 2023-08-28T02:15:28Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain -- This module requires the use of Module:List. local p = {} -- Local function which is used to get a correctly formatted entry. -- Function checks if the array had a value added by checking the counter, -- and returns the relevant result. local function getFormattedEntry(args, counter) if (counter == 1) then -- Check if the counter stayed the same. return "" -- Nothing was added to array; Return empty string. elseif (counter == 2) then -- Check if only one value was added to the array. return args[1] -- Only one value was added to array; Return that value. else -- The array had more than one value added. return table.concat(args, "<br/>") -- Tetrieve the formatted plainlist. end end --[[ Local function which is used to format an appearance for a comic book, in the style of: Line 1: <comic book title> #<issue number> (with comic book title in italics) Line 2: <release date> For other usages, see createGenericEntry(). The function works with the following combinations: -- Only comic book title (example: "The Incredible Hulk"). -- Title and issue number (example: "The Incredible Hulk" and "181"). -- Title and release date (example: "The Incredible Hulk and "November 1974"). -- Title, issue number and release date (example: "The Incredible Hulk", "181" and "November 1974"). -- Only release date (example: "November 1974"). --]] local function createComicEntry(appearanceMajor, appearanceMinor, appearanceDate) local fullString = {} -- Variable to save the array. local counter = 1 -- Variable to save the array counter. if (appearanceMajor ~= nil) then -- Check if a comic book title was entered. if (appearanceMinor == nil) then -- A comic book title was entered; Check if a issue number was entered. fullString[counter] = appearanceMajor -- A issue was not entered; Add only the comic book title to the array. counter = counter + 1 -- Increment counter by one. else fullString[counter] = appearanceMajor .. " " .. appearanceMinor -- A issue was entered; Add both to the array. counter = counter + 1 -- Increment counter by one. end end if (appearanceDate ~= nil) then -- Check if a release date was entered. fullString[counter] = appearanceDate -- A release date was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry. end --[[ Local function which is used to format an appearance for most usages, including television, film, books, songs and games, in the style of: Line 1: <minor work title> (in quotes) (Minor works include: TV episodes, chapters, songs and game missions) Line 2: <major work title> (in italics) (Major works include: TV series, films, books, albums and games) Line 3: <release date> For comic book usages, see createComicEntry(). The function works with the following combinations: -- Only minor work title (example: "Live Together, Die Alone"). -- Minor work title and major work title (example: "Live Together, Die Alone" and "Lost"). -- Minor work title and release date (example: "Live Together, Die Alone" and "May 24, 2006"). -- Minor work title, major work title and release date (example: "Live Together, Die Alone", "Lost" and "May 24, 2006"). -- Only major work title (example: "Lost"). -- major work title and release date (example: "Lost" and "May 24, 2006"). -- Only release date (example: "May 24, 2006"). --]] local function createGenericEntry(appearanceMajor, appearanceMinor, appearanceDate) local fullString = {} -- Variable to save the array. local counter = 1 -- Variable to save the array counter. if (appearanceMinor ~= nil) then -- Check if a minor appearance was entered. fullString[counter] = appearanceMinor -- A minor appearance was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end if (appearanceMajor ~= nil) then -- Check if a major appearance was entered. fullString[counter] = appearanceMajor -- A major appearance was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end if (appearanceDate ~= nil) then -- Check if a release date was entered. fullString[counter] = appearanceDate -- A release date was entered; Add it to the array. counter = counter + 1 -- Increment counter by one. end return getFormattedEntry(fullString, counter) -- Call getFormattedEntry() to get a correctly formatted entry. end -- Local function which is used to format with a hash symbol comic book issues. -- For other minor works, see getFormattedGenericMinorWork(). local function getFormattedComicMinorWorkTitle(issue) if (issue ~= nil) then -- Check if the issue is not nil. if (string.find(issue, "#")) then -- Check if the issue already has a hash symbol. return issue -- Hash symbol already present; Return issue. else local formattedString = string.gsub(issue, "%d+", "#%1") -- Hash symbol not found; Add the symbol before the issue number. return formattedString -- Return issue. end else return nil -- issue is nil; Return nil. end end -- Local function which is used to format with quotes a minor work title of most types. -- For comic book issues, see getFormattedComicMinorWork() (see [MOS:MINORWORK]). local function getFormattedGenericMinorWorkTitle(title) if (title ~= nil) then -- Check if the title is not nil. return "\"" .. title .. "\"" -- Title is not nil; Add quotes to the title. else return nil -- Title is nil; Return nil. end end -- Local function which is used to format with italics a major work title (see [MOS:MAJORWORK]). local function getFormattedMajorWorkTitle(title) if (title ~= nil) then -- Check if the title is not nil. return "''" .. title .. "''" -- Title is not nil; Add italics to the title. else return nil -- Title is nil; Return nil. end end -- Local function which does the actual main process. local function _getFormattedAppearance(args) local appearanceMajor = args['major_work'] -- Get the title of the major work. local appearanceMinor = args['minor_work'] -- Get the title of the minor work. local isComic = false -- Variable to save the status of wether the appearence is from a comic book. if (args['issue'] ~= nil) then -- Check if the comic specific issue is not nil. appearanceMinor = args['issue'] -- Issue is not nil; Get the issue number. isComic = true -- Set isComic to true. end local appearanceDate = args['date'] -- Get the release date of the minor work. local formattedAppearanceMajor = getFormattedMajorWorkTitle(appearanceMajor) -- Call getFormattedMajorWorkTitle() to get a formatted major work title. if (isComic == false) then -- Check if the appearance is a comic book appearance. -- The appearance is not a comic book appearance; local formattedAppearanceMinor = getFormattedGenericMinorWorkTitle(appearanceMinor) -- Call getFormattedGenericMinorWorkTitle() to get a formatted minor work title. return createGenericEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createGenericEntry() to create an appearance entry. else -- The appearance is a comic book appearance. local formattedAppearanceMinor = getFormattedComicMinorWorkTitle(appearanceMinor) -- Call getFormattedComicMinorWorkTitle() to get a formatted minor work title. return createComicEntry(formattedAppearanceMajor, formattedAppearanceMinor, appearanceDate) -- Call createComicEntry() to create a comic book appearance entry. end end --[[ Public function which is used to format the |first_appeared= and |last_appeared= fields. The usage of this module allows for correct title formatting (see [MOS:MAJORWORK] and [MOS:MINORWORK]), and correct line breaks based on guidelines (see [WP:UBLIST]). Parameters: -- |major_work= — optional; The title of the major work the fictional element appeared in. Major works include TV series, films, books, albums and games. -- |minor_work= — optional; The title of the minor work the fictional element appeared in. Minor works include TV episodes, chapters, songs and game missions. -- |issue= — optional; The number of the comic book issue the fictional element appeared in. -- |date= — optional; The date of the publication/release of the minor work where the fictional element appeared in. --]] function p.getFormattedAppearance(frame) local getArgs = require('Module:Arguments').getArgs -- Use Module:Arguments to access module arguments. local args = getArgs(frame) -- Get the arguments sent via the template. return _getFormattedAppearance(args) -- Call _getFormattedAppearance() to perform the actual process. end return p 983d4add2379f19ec30241c0470bf9b6c4089eb2 Module:Message box 828 90 173 172 2023-08-28T02:15:29Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain -- This is a meta-module for producing message box templates, including -- {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}. -- Load necessary modules. require('Module:No globals') local getArgs local yesno = require('Module:Yesno') local templatestyles = 'Module:Message box/styles.css' -- Get a language object for formatDate and ucfirst. local lang = mw.language.getContentLanguage() -- Define constants local CONFIG_MODULE = 'Module:Message box/configuration' local DEMOSPACES = {user = 'tmbox', talk = 'tmbox', image = 'imbox', file = 'imbox', category = 'cmbox', article = 'ambox', main = 'ambox'} -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function getTitleObject(...) -- Get the title object, passing the function through pcall -- in case we are over the expensive function count limit. local success, title = pcall(mw.title.new, ...) if success then return title end end local function union(t1, t2) -- Returns the union of two arrays. local vals = {} for i, v in ipairs(t1) do vals[v] = true end for i, v in ipairs(t2) do vals[v] = true end local ret = {} for k in pairs(vals) do table.insert(ret, k) end table.sort(ret) return ret end local function getArgNums(args, prefix) local nums = {} for k, v in pairs(args) do local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$') if num then table.insert(nums, tonumber(num)) end end table.sort(nums) return nums end -------------------------------------------------------------------------------- -- Box class definition -------------------------------------------------------------------------------- local MessageBox = {} MessageBox.__index = MessageBox function MessageBox.new(boxType, args, cfg) args = args or {} local obj = {} -- Set the title object and the namespace. obj.title = getTitleObject(args.page) or mw.title.getCurrentTitle() -- Set the config for our box type. obj.cfg = cfg[boxType] if not obj.cfg then local ns = obj.title.namespace -- boxType is "mbox" or invalid input if args.demospace and args.demospace ~= '' then -- implement demospace parameter of mbox local demospace = string.lower(args.demospace) if DEMOSPACES[demospace] then -- use template from DEMOSPACES obj.cfg = cfg[DEMOSPACES[demospace]] elseif string.find( demospace, 'talk' ) then -- demo as a talk page obj.cfg = cfg.tmbox else -- default to ombox obj.cfg = cfg.ombox end elseif ns == 0 then obj.cfg = cfg.ambox -- main namespace elseif ns == 6 then obj.cfg = cfg.imbox -- file namespace elseif ns == 14 then obj.cfg = cfg.cmbox -- category namespace else local nsTable = mw.site.namespaces[ns] if nsTable and nsTable.isTalk then obj.cfg = cfg.tmbox -- any talk namespace else obj.cfg = cfg.ombox -- other namespaces or invalid input end end end -- Set the arguments, and remove all blank arguments except for the ones -- listed in cfg.allowBlankParams. do local newArgs = {} for k, v in pairs(args) do if v ~= '' then newArgs[k] = v end end for i, param in ipairs(obj.cfg.allowBlankParams or {}) do newArgs[param] = args[param] end obj.args = newArgs end -- Define internal data structure. obj.categories = {} obj.classes = {} -- For lazy loading of [[Module:Category handler]]. obj.hasCategories = false return setmetatable(obj, MessageBox) end function MessageBox:addCat(ns, cat, sort) if not cat then return nil end if sort then cat = string.format('[[Category:%s|%s]]', cat, sort) else cat = string.format('[[Category:%s]]', cat) end self.hasCategories = true self.categories[ns] = self.categories[ns] or {} table.insert(self.categories[ns], cat) end function MessageBox:addClass(class) if not class then return nil end table.insert(self.classes, class) end function MessageBox:setParameters() local args = self.args local cfg = self.cfg -- Get type data. self.type = args.type local typeData = cfg.types[self.type] self.invalidTypeError = cfg.showInvalidTypeError and self.type and not typeData typeData = typeData or cfg.types[cfg.default] self.typeClass = typeData.class self.typeImage = typeData.image -- Find whether we are using a small message box. self.isSmall = cfg.allowSmall and ( cfg.smallParam and args.small == cfg.smallParam or not cfg.smallParam and yesno(args.small) ) -- Add attributes, classes and styles. self.id = args.id self.name = args.name if self.name then self:addClass('box-' .. string.gsub(self.name,' ','_')) end if yesno(args.plainlinks) ~= false then self:addClass('plainlinks') end for _, class in ipairs(cfg.classes or {}) do self:addClass(class) end if self.isSmall then self:addClass(cfg.smallClass or 'mbox-small') end self:addClass(self.typeClass) self:addClass(args.class) self.style = args.style self.attrs = args.attrs -- Set text style. self.textstyle = args.textstyle -- Find if we are on the template page or not. This functionality is only -- used if useCollapsibleTextFields is set, or if both cfg.templateCategory -- and cfg.templateCategoryRequireName are set. self.useCollapsibleTextFields = cfg.useCollapsibleTextFields if self.useCollapsibleTextFields or cfg.templateCategory and cfg.templateCategoryRequireName then if self.name then local templateName = mw.ustring.match( self.name, '^[tT][eE][mM][pP][lL][aA][tT][eE][%s_]*:[%s_]*(.*)$' ) or self.name templateName = 'Template:' .. templateName self.templateTitle = getTitleObject(templateName) end self.isTemplatePage = self.templateTitle and mw.title.equals(self.title, self.templateTitle) end -- Process data for collapsible text fields. At the moment these are only -- used in {{ambox}}. if self.useCollapsibleTextFields then -- Get the self.issue value. if self.isSmall and args.smalltext then self.issue = args.smalltext else local sect if args.sect == '' then sect = 'This ' .. (cfg.sectionDefault or 'page') elseif type(args.sect) == 'string' then sect = 'This ' .. args.sect end local issue = args.issue issue = type(issue) == 'string' and issue ~= '' and issue or nil local text = args.text text = type(text) == 'string' and text or nil local issues = {} table.insert(issues, sect) table.insert(issues, issue) table.insert(issues, text) self.issue = table.concat(issues, ' ') end -- Get the self.talk value. local talk = args.talk -- Show talk links on the template page or template subpages if the talk -- parameter is blank. if talk == '' and self.templateTitle and ( mw.title.equals(self.templateTitle, self.title) or self.title:isSubpageOf(self.templateTitle) ) then talk = '#' elseif talk == '' then talk = nil end if talk then -- If the talk value is a talk page, make a link to that page. Else -- assume that it's a section heading, and make a link to the talk -- page of the current page with that section heading. local talkTitle = getTitleObject(talk) local talkArgIsTalkPage = true if not talkTitle or not talkTitle.isTalkPage then talkArgIsTalkPage = false talkTitle = getTitleObject( self.title.text, mw.site.namespaces[self.title.namespace].talk.id ) end if talkTitle and talkTitle.exists then local talkText = 'Relevant discussion may be found on' if talkArgIsTalkPage then talkText = string.format( '%s [[%s|%s]].', talkText, talk, talkTitle.prefixedText ) else talkText = string.format( '%s the [[%s#%s|talk page]].', talkText, talkTitle.prefixedText, talk ) end self.talk = talkText end end -- Get other values. self.fix = args.fix ~= '' and args.fix or nil local date if args.date and args.date ~= '' then date = args.date elseif args.date == '' and self.isTemplatePage then date = lang:formatDate('F Y') end if date then self.date = string.format(" <small class='date-container'>''(<span class='date'>%s</span>)''</small>", date) end self.info = args.info end -- Set the non-collapsible text field. At the moment this is used by all box -- types other than ambox, and also by ambox when small=yes. if self.isSmall then self.text = args.smalltext or args.text else self.text = args.text end -- Set the below row. self.below = cfg.below and args.below -- General image settings. self.imageCellDiv = not self.isSmall and cfg.imageCellDiv self.imageEmptyCell = cfg.imageEmptyCell if cfg.imageEmptyCellStyle then self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px' end -- Left image settings. local imageLeft = self.isSmall and args.smallimage or args.image if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none' or not cfg.imageCheckBlank and imageLeft ~= 'none' then self.imageLeft = imageLeft if not imageLeft then local imageSize = self.isSmall and (cfg.imageSmallSize or '30x30px') or '40x40px' self.imageLeft = string.format('[[File:%s|%s|link=|alt=]]', self.typeImage or 'Imbox notice.png', imageSize) end end -- Right image settings. local imageRight = self.isSmall and args.smallimageright or args.imageright if not (cfg.imageRightNone and imageRight == 'none') then self.imageRight = imageRight end end function MessageBox:setMainspaceCategories() local args = self.args local cfg = self.cfg if not cfg.allowMainspaceCategories then return nil end local nums = {} for _, prefix in ipairs{'cat', 'category', 'all'} do args[prefix .. '1'] = args[prefix] nums = union(nums, getArgNums(args, prefix)) end -- The following is roughly equivalent to the old {{Ambox/category}}. local date = args.date date = type(date) == 'string' and date local preposition = 'from' for _, num in ipairs(nums) do local mainCat = args['cat' .. tostring(num)] or args['category' .. tostring(num)] local allCat = args['all' .. tostring(num)] mainCat = type(mainCat) == 'string' and mainCat allCat = type(allCat) == 'string' and allCat if mainCat and date and date ~= '' then local catTitle = string.format('%s %s %s', mainCat, preposition, date) self:addCat(0, catTitle) catTitle = getTitleObject('Category:' .. catTitle) if not catTitle or not catTitle.exists then self:addCat(0, 'Articles with invalid date parameter in template') end elseif mainCat and (not date or date == '') then self:addCat(0, mainCat) end if allCat then self:addCat(0, allCat) end end end function MessageBox:setTemplateCategories() local args = self.args local cfg = self.cfg -- Add template categories. if cfg.templateCategory then if cfg.templateCategoryRequireName then if self.isTemplatePage then self:addCat(10, cfg.templateCategory) end elseif not self.title.isSubpage then self:addCat(10, cfg.templateCategory) end end -- Add template error categories. if cfg.templateErrorCategory then local templateErrorCategory = cfg.templateErrorCategory local templateCat, templateSort if not self.name and not self.title.isSubpage then templateCat = templateErrorCategory elseif self.isTemplatePage then local paramsToCheck = cfg.templateErrorParamsToCheck or {} local count = 0 for i, param in ipairs(paramsToCheck) do if not args[param] then count = count + 1 end end if count > 0 then templateCat = templateErrorCategory templateSort = tostring(count) end if self.categoryNums and #self.categoryNums > 0 then templateCat = templateErrorCategory templateSort = 'C' end end self:addCat(10, templateCat, templateSort) end end function MessageBox:setAllNamespaceCategories() -- Set categories for all namespaces. if self.invalidTypeError then local allSort = (self.title.namespace == 0 and 'Main:' or '') .. self.title.prefixedText self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort) end end function MessageBox:setCategories() if self.title.namespace == 0 then self:setMainspaceCategories() elseif self.title.namespace == 10 then self:setTemplateCategories() end self:setAllNamespaceCategories() end function MessageBox:renderCategories() if not self.hasCategories then -- No categories added, no need to pass them to Category handler so, -- if it was invoked, it would return the empty string. -- So we shortcut and return the empty string. return "" end -- Convert category tables to strings and pass them through -- [[Module:Category handler]]. return require('Module:Category handler')._main{ main = table.concat(self.categories[0] or {}), template = table.concat(self.categories[10] or {}), all = table.concat(self.categories.all or {}), nocat = self.args.nocat, page = self.args.page } end function MessageBox:export() local root = mw.html.create() -- Create the box table. local boxTable = root:tag('table') boxTable:attr('id', self.id or nil) for i, class in ipairs(self.classes or {}) do boxTable:addClass(class or nil) end boxTable :cssText(self.style or nil) :attr('role', 'presentation') if self.attrs then boxTable:attr(self.attrs) end -- Add the left-hand image. local row = boxTable:tag('tr') if self.imageLeft then local imageLeftCell = row:tag('td'):addClass('mbox-image') if self.imageCellDiv then -- If we are using a div, redefine imageLeftCell so that the image -- is inside it. Divs use style="width: 52px;", which limits the -- image width to 52px. If any images in a div are wider than that, -- they may overlap with the text or cause other display problems. imageLeftCell = imageLeftCell:tag('div'):css('width', '52px') end imageLeftCell:wikitext(self.imageLeft or nil) elseif self.imageEmptyCell then -- Some message boxes define an empty cell if no image is specified, and -- some don't. The old template code in templates where empty cells are -- specified gives the following hint: "No image. Cell with some width -- or padding necessary for text cell to have 100% width." row:tag('td') :addClass('mbox-empty-cell') :cssText(self.imageEmptyCellStyle or nil) end -- Add the text. local textCell = row:tag('td'):addClass('mbox-text') if self.useCollapsibleTextFields then -- The message box uses advanced text parameters that allow things to be -- collapsible. At the moment, only ambox uses this. textCell:cssText(self.textstyle or nil) local textCellDiv = textCell:tag('div') textCellDiv :addClass('mbox-text-span') :wikitext(self.issue or nil) if (self.talk or self.fix) and not self.isSmall then textCellDiv:tag('span') :addClass('hide-when-compact') :wikitext(self.talk and (' ' .. self.talk) or nil) :wikitext(self.fix and (' ' .. self.fix) or nil) end textCellDiv:wikitext(self.date and (' ' .. self.date) or nil) if self.info and not self.isSmall then textCellDiv :tag('span') :addClass('hide-when-compact') :wikitext(self.info and (' ' .. self.info) or nil) end else -- Default text formatting - anything goes. textCell :cssText(self.textstyle or nil) :wikitext(self.text or nil) end -- Add the right-hand image. if self.imageRight then local imageRightCell = row:tag('td'):addClass('mbox-imageright') if self.imageCellDiv then -- If we are using a div, redefine imageRightCell so that the image -- is inside it. imageRightCell = imageRightCell:tag('div'):css('width', '52px') end imageRightCell :wikitext(self.imageRight or nil) end -- Add the below row. if self.below then boxTable:tag('tr') :tag('td') :attr('colspan', self.imageRight and '3' or '2') :addClass('mbox-text') :cssText(self.textstyle or nil) :wikitext(self.below or nil) end -- Add error message for invalid type parameters. if self.invalidTypeError then root:tag('div') :css('text-align', 'center') :wikitext(string.format( 'This message box is using an invalid "type=%s" parameter and needs fixing.', self.type or '' )) end -- Add categories. root:wikitext(self:renderCategories() or nil) return tostring(root) end -------------------------------------------------------------------------------- -- Exports -------------------------------------------------------------------------------- local p, mt = {}, {} function p._exportClasses() -- For testing. return { MessageBox = MessageBox } end function p.main(boxType, args, cfgTables) local box = MessageBox.new(boxType, args, cfgTables or mw.loadData(CONFIG_MODULE)) box:setParameters() box:setCategories() return box:export() end local function templatestyles(frame, src) return mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = { src = templatestyles} } .. 'CONFIG_MODULE' end function mt.__index(t, k) return function (frame) if not getArgs then getArgs = require('Module:Arguments').getArgs end return t.main(k, getArgs(frame, {trim = false, removeBlanks = false})) end end return setmetatable(p, mt) be00cd389f9f2afcd361e5d5e33622839555cbd9 Module:Message box/configuration 828 91 175 174 2023-08-28T02:15:29Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain -------------------------------------------------------------------------------- -- Message box configuration -- -- -- -- This module contains configuration data for [[Module:Message box]]. -- -------------------------------------------------------------------------------- return { ambox = { types = { speedy = { class = 'ambox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'ambox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'ambox-content', image = 'Ambox important.svg' }, style = { class = 'ambox-style', image = 'Edit-clear.svg' }, move = { class = 'ambox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'ambox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'ambox-notice', image = 'Information icon4.svg' } }, default = 'notice', allowBlankParams = {'talk', 'sect', 'date', 'issue', 'fix', 'hidden'}, allowSmall = true, smallParam = 'left', smallClass = 'mbox-small-left', classes = {'metadata', 'ambox'}, imageEmptyCell = true, imageCheckBlank = true, imageSmallSize = '20x20px', imageCellDiv = true, useCollapsibleTextFields = true, imageRightNone = true, sectionDefault = 'article', allowMainspaceCategories = true, templateCategory = 'Article message templates', templateCategoryRequireName = true, templateErrorCategory = 'Article message templates with missing parameters', templateErrorParamsToCheck = {'issue', 'fix'}, }, cmbox = { types = { speedy = { class = 'cmbox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'cmbox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'cmbox-content', image = 'Ambox important.svg' }, style = { class = 'cmbox-style', image = 'Edit-clear.svg' }, move = { class = 'cmbox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'cmbox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'cmbox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'cmbox'}, imageEmptyCell = true }, fmbox = { types = { warning = { class = 'fmbox-warning', image = 'Ambox warning pn.svg' }, editnotice = { class = 'fmbox-editnotice', image = 'Information icon4.svg' }, system = { class = 'fmbox-system', image = 'Information icon4.svg' } }, default = 'system', showInvalidTypeError = true, classes = {'fmbox'}, imageEmptyCell = false, imageRightNone = false }, imbox = { types = { speedy = { class = 'imbox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'imbox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'imbox-content', image = 'Ambox important.svg' }, style = { class = 'imbox-style', image = 'Edit-clear.svg' }, move = { class = 'imbox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'imbox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, license = { class = 'imbox-license licensetpl', image = 'Imbox license.png' -- @todo We need an SVG version of this }, featured = { class = 'imbox-featured', image = 'Cscr-featured.svg' }, notice = { class = 'imbox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'imbox'}, imageEmptyCell = true, below = true, templateCategory = 'File message boxes' }, ombox = { types = { speedy = { class = 'ombox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'ombox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'ombox-content', image = 'Ambox important.svg' }, style = { class = 'ombox-style', image = 'Edit-clear.svg' }, move = { class = 'ombox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'ombox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'ombox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'ombox'}, allowSmall = true, imageEmptyCell = true, imageRightNone = true }, tmbox = { types = { speedy = { class = 'tmbox-speedy', image = 'Ambox warning pn.svg' }, delete = { class = 'tmbox-delete', image = 'Ambox warning pn.svg' }, content = { class = 'tmbox-content', image = 'Ambox important.svg' }, style = { class = 'tmbox-style', image = 'Edit-clear.svg' }, move = { class = 'tmbox-move', image = 'Merge-split-transwiki default.svg' }, protection = { class = 'tmbox-protection', image = 'Semi-protection-shackle-keyhole.svg' }, notice = { class = 'tmbox-notice', image = 'Information icon4.svg' } }, default = 'notice', showInvalidTypeError = true, classes = {'tmbox'}, allowSmall = true, imageRightNone = true, imageEmptyCell = true, imageEmptyCellStyle = true, templateCategory = 'Talk message boxes' } } c6bd9191861b23e474e12b19c694335c4bc3af5f Module:TNT 828 92 177 176 2023-08-28T02:15:29Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain -- -- INTRO: (!!! DO NOT RENAME THIS PAGE !!!) -- This module allows any template or module to be copy/pasted between -- wikis without any translation changes. All translation text is stored -- in the global Data:*.tab pages on Commons, and used everywhere. -- -- SEE: https://www.mediawiki.org/wiki/Multilingual_Templates_and_Modules -- -- ATTENTION: -- Please do NOT rename this module - it has to be identical on all wikis. -- This code is maintained at https://www.mediawiki.org/wiki/Module:TNT -- Please do not modify it anywhere else, as it may get copied and override your changes. -- Suggestions can be made at https://www.mediawiki.org/wiki/Module_talk:TNT -- -- DESCRIPTION: -- The "msg" function uses a Commons dataset to translate a message -- with a given key (e.g. source-table), plus optional arguments -- to the wiki markup in the current content language. -- Use lang=xx to set language. Example: -- -- {{#invoke:TNT | msg -- | I18n/Template:Graphs.tab <!-- https://commons.wikimedia.org/wiki/Data:I18n/Template:Graphs.tab --> -- | source-table <!-- uses a translation message with id = "source-table" --> -- | param1 }} <!-- optional parameter --> -- -- -- The "doc" function will generate the <templatedata> parameter documentation for templates. -- This way all template parameters can be stored and localized in a single Commons dataset. -- NOTE: "doc" assumes that all documentation is located in Data:Templatedata/* on Commons. -- -- {{#invoke:TNT | doc | Graph:Lines }} -- uses https://commons.wikimedia.org/wiki/Data:Templatedata/Graph:Lines.tab -- if the current page is Template:Graph:Lines/doc -- local p = {} local i18nDataset = 'I18n/Module:TNT.tab' -- Forward declaration of the local functions local sanitizeDataset, loadData, link, formatMessage function p.msg(frame) local dataset, id local params = {} local lang = nil for k, v in pairs(frame.args) do if k == 1 then dataset = mw.text.trim(v) elseif k == 2 then id = mw.text.trim(v) elseif type(k) == 'number' then params[k - 2] = mw.text.trim(v) elseif k == 'lang' and v ~= '_' then lang = mw.text.trim(v) end end return formatMessage(dataset, id, params, lang) end -- Identical to p.msg() above, but used from other lua modules -- Parameters: name of dataset, message key, optional arguments -- Example with 2 params: format('I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset') function p.format(dataset, key, ...) local checkType = require('libraryUtil').checkType checkType('format', 1, dataset, 'string') checkType('format', 2, key, 'string') return formatMessage(dataset, key, {...}) end -- Identical to p.msg() above, but used from other lua modules with the language param -- Parameters: language code, name of dataset, message key, optional arguments -- Example with 2 params: formatInLanguage('es', I18n/Module:TNT', 'error_bad_msgkey', 'my-key', 'my-dataset') function p.formatInLanguage(lang, dataset, key, ...) local checkType = require('libraryUtil').checkType checkType('formatInLanguage', 1, lang, 'string') checkType('formatInLanguage', 2, dataset, 'string') checkType('formatInLanguage', 3, key, 'string') return formatMessage(dataset, key, {...}, lang) end -- Obsolete function that adds a 'c:' prefix to the first param. -- "Sandbox/Sample.tab" -> 'c:Data:Sandbox/Sample.tab' function p.link(frame) return link(frame.args[1]) end function p.doc(frame) local dataset = 'Templatedata/' .. sanitizeDataset(frame.args[1]) return frame:extensionTag('templatedata', p.getTemplateData(dataset)) .. formatMessage(i18nDataset, 'edit_doc', {link(dataset)}) end function p.getTemplateData(dataset) -- TODO: add '_' parameter once lua starts reindexing properly for "all" languages local data = loadData(dataset) local names = {} for _, field in ipairs(data.schema.fields) do table.insert(names, field.name) end local params = {} local paramOrder = {} for _, row in ipairs(data.data) do local newVal = {} local name = nil for pos, columnName in ipairs(names) do if columnName == 'name' then name = row[pos] else newVal[columnName] = row[pos] end end if name then params[name] = newVal table.insert(paramOrder, name) end end -- Work around json encoding treating {"1":{...}} as an [{...}] params['zzz123']='' local json = mw.text.jsonEncode({ params=params, paramOrder=paramOrder, description=data.description }) json = string.gsub(json,'"zzz123":"",?', "") return json end -- Local functions sanitizeDataset = function(dataset) if not dataset then return nil end dataset = mw.text.trim(dataset) if dataset == '' then return nil elseif string.sub(dataset,-4) ~= '.tab' then return dataset .. '.tab' else return dataset end end loadData = function(dataset, lang) dataset = sanitizeDataset(dataset) if not dataset then error(formatMessage(i18nDataset, 'error_no_dataset', {})) end -- Give helpful error to thirdparties who try and copy this module. if not mw.ext or not mw.ext.data or not mw.ext.data.get then error(string.format([['''Missing JsonConfig extension, or not properly configured; Cannot load https://commons.wikimedia.org/wiki/Data:%s. Please properly enable the JSONConfig extension at Special:ManageWiki/extensions#mw-prefsection-other See https://www.mediawiki.org/wiki/Extension:JsonConfig#Supporting_Wikimedia_templates''']], dataset)) end local data = mw.ext.data.get(dataset, lang) if data == false then if dataset == i18nDataset then -- Prevent cyclical calls error('Missing Commons dataset ' .. i18nDataset) else error(formatMessage(i18nDataset, 'error_bad_dataset', {link(dataset)})) end end return data end -- Given a dataset name, convert it to a title with the 'commons:data:' prefix link = function(dataset) return 'c:Data:' .. mw.text.trim(dataset or '') end formatMessage = function(dataset, key, params, lang) for _, row in pairs(loadData(dataset, lang).data) do local id, msg = unpack(row) if id == key then local result = mw.message.newRawMessage(msg, unpack(params or {})) return result:plain() end end if dataset == i18nDataset then -- Prevent cyclical calls error('Invalid message key "' .. key .. '"') else error(formatMessage(i18nDataset, 'error_bad_msgkey', {key, link(dataset)})) end end return p 6d981852d69d5958a60d96d24c311680564c6103 Module:String 828 93 179 178 2023-08-28T02:15:30Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain --[[ This module is intended to provide access to basic string functions. Most of the functions provided here can be invoked with named parameters, unnamed parameters, or a mixture. If named parameters are used, Mediawiki will automatically remove any leading or trailing whitespace from the parameter. Depending on the intended use, it may be advantageous to either preserve or remove such whitespace. Global options ignore_errors: If set to 'true' or 1, any error condition will result in an empty string being returned rather than an error message. error_category: If an error occurs, specifies the name of a category to include with the error message. The default category is [Category:Errors reported by Module String]. no_category: If set to 'true' or 1, no category will be added if an error is generated. Unit tests for this module are available at Module:String/tests. ]] local str = {} --[[ len This function returns the length of the target string. Usage: {{#invoke:String|len|target_string|}} OR {{#invoke:String|len|s=target_string}} Parameters s: The string whose length to report If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from the target string. ]] function str.len( frame ) local new_args = str._getParameters( frame.args, {'s'} ) local s = new_args['s'] or '' return mw.ustring.len( s ) end --[[ sub This function returns a substring of the target string at specified indices. Usage: {{#invoke:String|sub|target_string|start_index|end_index}} OR {{#invoke:String|sub|s=target_string|i=start_index|j=end_index}} Parameters s: The string to return a subset of i: The fist index of the substring to return, defaults to 1. j: The last index of the string to return, defaults to the last character. The first character of the string is assigned an index of 1. If either i or j is a negative value, it is interpreted the same as selecting a character by counting from the end of the string. Hence, a value of -1 is the same as selecting the last character of the string. If the requested indices are out of range for the given string, an error is reported. ]] function str.sub( frame ) local new_args = str._getParameters( frame.args, { 's', 'i', 'j' } ) local s = new_args['s'] or '' local i = tonumber( new_args['i'] ) or 1 local j = tonumber( new_args['j'] ) or -1 local len = mw.ustring.len( s ) -- Convert negatives for range checking if i < 0 then i = len + i + 1 end if j < 0 then j = len + j + 1 end if i > len or j > len or i < 1 or j < 1 then return str._error( 'String subset index out of range' ) end if j < i then return str._error( 'String subset indices out of order' ) end return mw.ustring.sub( s, i, j ) end --[[ _match This function returns a substring from the source string that matches a specified pattern. It is exported for use in other modules Usage: strmatch = require("Module:String")._match sresult = strmatch( s, pattern, start, match, plain, nomatch ) Parameters s: The string to search pattern: The pattern or string to find within the string start: The index within the source string to start the search. The first character of the string has index 1. Defaults to 1. match: In some cases it may be possible to make multiple matches on a single string. This specifies which match to return, where the first match is match= 1. If a negative number is specified then a match is returned counting from the last match. Hence match = -1 is the same as requesting the last match. Defaults to 1. plain: A flag indicating that the pattern should be understood as plain text. Defaults to false. nomatch: If no match is found, output the "nomatch" value rather than an error. For information on constructing Lua patterns, a form of [regular expression], see: * http://www.lua.org/manual/5.1/manual.html#5.4.1 * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns ]] -- This sub-routine is exported for use in other modules function str._match( s, pattern, start, match_index, plain_flag, nomatch ) if s == '' then return str._error( 'Target string is empty' ) end if pattern == '' then return str._error( 'Pattern string is empty' ) end start = tonumber(start) or 1 if math.abs(start) < 1 or math.abs(start) > mw.ustring.len( s ) then return str._error( 'Requested start is out of range' ) end if match_index == 0 then return str._error( 'Match index is out of range' ) end if plain_flag then pattern = str._escapePattern( pattern ) end local result if match_index == 1 then -- Find first match is simple case result = mw.ustring.match( s, pattern, start ) else if start > 1 then s = mw.ustring.sub( s, start ) end local iterator = mw.ustring.gmatch(s, pattern) if match_index > 0 then -- Forward search for w in iterator do match_index = match_index - 1 if match_index == 0 then result = w break end end else -- Reverse search local result_table = {} local count = 1 for w in iterator do result_table[count] = w count = count + 1 end result = result_table[ count + match_index ] end end if result == nil then if nomatch == nil then return str._error( 'Match not found' ) else return nomatch end else return result end end --[[ match This function returns a substring from the source string that matches a specified pattern. Usage: {{#invoke:String|match|source_string|pattern_string|start_index|match_number|plain_flag|nomatch_output}} OR {{#invoke:String|match|s=source_string|pattern=pattern_string|start=start_index |match=match_number|plain=plain_flag|nomatch=nomatch_output}} Parameters s: The string to search pattern: The pattern or string to find within the string start: The index within the source string to start the search. The first character of the string has index 1. Defaults to 1. match: In some cases it may be possible to make multiple matches on a single string. This specifies which match to return, where the first match is match= 1. If a negative number is specified then a match is returned counting from the last match. Hence match = -1 is the same as requesting the last match. Defaults to 1. plain: A flag indicating that the pattern should be understood as plain text. Defaults to false. nomatch: If no match is found, output the "nomatch" value rather than an error. If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from each string. In some circumstances this is desirable, in other cases one may want to preserve the whitespace. If the match_number or start_index are out of range for the string being queried, then this function generates an error. An error is also generated if no match is found. If one adds the parameter ignore_errors=true, then the error will be suppressed and an empty string will be returned on any failure. For information on constructing Lua patterns, a form of [regular expression], see: * http://www.lua.org/manual/5.1/manual.html#5.4.1 * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns * http://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Ustring_patterns ]] -- This is the entry point for #invoke:String|match function str.match( frame ) local new_args = str._getParameters( frame.args, {'s', 'pattern', 'start', 'match', 'plain', 'nomatch'} ) local s = new_args['s'] or '' local start = tonumber( new_args['start'] ) or 1 local plain_flag = str._getBoolean( new_args['plain'] or false ) local pattern = new_args['pattern'] or '' local match_index = math.floor( tonumber(new_args['match']) or 1 ) local nomatch = new_args['nomatch'] return str._match( s, pattern, start, match_index, plain_flag, nomatch ) end --[[ pos This function returns a single character from the target string at position pos. Usage: {{#invoke:String|pos|target_string|index_value}} OR {{#invoke:String|pos|target=target_string|pos=index_value}} Parameters target: The string to search pos: The index for the character to return If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from the target string. In some circumstances this is desirable, in other cases one may want to preserve the whitespace. The first character has an index value of 1. If one requests a negative value, this function will select a character by counting backwards from the end of the string. In other words pos = -1 is the same as asking for the last character. A requested value of zero, or a value greater than the length of the string returns an error. ]] function str.pos( frame ) local new_args = str._getParameters( frame.args, {'target', 'pos'} ) local target_str = new_args['target'] or '' local pos = tonumber( new_args['pos'] ) or 0 if pos == 0 or math.abs(pos) > mw.ustring.len( target_str ) then return str._error( 'String index out of range' ) end return mw.ustring.sub( target_str, pos, pos ) end --[[ find This function allows one to search for a target string or pattern within another string. Usage: {{#invoke:String|find|source_str|target_string|start_index|plain_flag}} OR {{#invoke:String|find|source=source_str|target=target_str|start=start_index|plain=plain_flag}} Parameters source: The string to search target: The string or pattern to find within source start: The index within the source string to start the search, defaults to 1 plain: Boolean flag indicating that target should be understood as plain text and not as a Lua style regular expression, defaults to true If invoked using named parameters, Mediawiki will automatically remove any leading or trailing whitespace from the parameter. In some circumstances this is desirable, in other cases one may want to preserve the whitespace. This function returns the first index >= "start" where "target" can be found within "source". Indices are 1-based. If "target" is not found, then this function returns 0. If either "source" or "target" are missing / empty, this function also returns 0. This function should be safe for UTF-8 strings. ]] function str.find( frame ) local new_args = str._getParameters( frame.args, {'source', 'target', 'start', 'plain' } ) local source_str = new_args['source'] or '' local pattern = new_args['target'] or '' local start_pos = tonumber(new_args['start']) or 1 local plain = new_args['plain'] or true if source_str == '' or pattern == '' then return 0 end plain = str._getBoolean( plain ) local start = mw.ustring.find( source_str, pattern, start_pos, plain ) if start == nil then start = 0 end return start end --[[ replace This function allows one to replace a target string or pattern within another string. Usage: {{#invoke:String|replace|source_str|pattern_string|replace_string|replacement_count|plain_flag}} OR {{#invoke:String|replace|source=source_string|pattern=pattern_string|replace=replace_string| count=replacement_count|plain=plain_flag}} Parameters source: The string to search pattern: The string or pattern to find within source replace: The replacement text count: The number of occurences to replace, defaults to all. plain: Boolean flag indicating that pattern should be understood as plain text and not as a Lua style regular expression, defaults to true ]] function str.replace( frame ) local new_args = str._getParameters( frame.args, {'source', 'pattern', 'replace', 'count', 'plain' } ) local source_str = new_args['source'] or '' local pattern = new_args['pattern'] or '' local replace = new_args['replace'] or '' local count = tonumber( new_args['count'] ) local plain = new_args['plain'] or true if source_str == '' or pattern == '' then return source_str end plain = str._getBoolean( plain ) if plain then pattern = str._escapePattern( pattern ) replace = mw.ustring.gsub( replace, "%%", "%%%%" ) --Only need to escape replacement sequences. end local result if count ~= nil then result = mw.ustring.gsub( source_str, pattern, replace, count ) else result = mw.ustring.gsub( source_str, pattern, replace ) end return result end --[[ simple function to pipe string.rep to templates. ]] function str.rep( frame ) local repetitions = tonumber( frame.args[2] ) if not repetitions then return str._error( 'function rep expects a number as second parameter, received "' .. ( frame.args[2] or '' ) .. '"' ) end return string.rep( frame.args[1] or '', repetitions ) end --[[ escapePattern This function escapes special characters from a Lua string pattern. See [1] for details on how patterns work. [1] https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#Patterns Usage: {{#invoke:String|escapePattern|pattern_string}} Parameters pattern_string: The pattern string to escape. ]] function str.escapePattern( frame ) local pattern_str = frame.args[1] if not pattern_str then return str._error( 'No pattern string specified' ) end local result = str._escapePattern( pattern_str ) return result end --[[ count This function counts the number of occurrences of one string in another. ]] function str.count(frame) local args = str._getParameters(frame.args, {'source', 'pattern', 'plain'}) local source = args.source or '' local pattern = args.pattern or '' local plain = str._getBoolean(args.plain or true) if plain then pattern = str._escapePattern(pattern) end local _, count = mw.ustring.gsub(source, pattern, '') return count end --[[ endswith This function determines whether a string ends with another string. ]] function str.endswith(frame) local args = str._getParameters(frame.args, {'source', 'pattern'}) local source = args.source or '' local pattern = args.pattern or '' if pattern == '' then -- All strings end with the empty string. return "yes" end if mw.ustring.sub(source, -mw.ustring.len(pattern), -1) == pattern then return "yes" else return "" end end --[[ join Join all non empty arguments together; the first argument is the separator. Usage: {{#invoke:String|join|sep|one|two|three}} ]] function str.join(frame) local args = {} local sep for _, v in ipairs( frame.args ) do if sep then if v ~= '' then table.insert(args, v) end else sep = v end end return table.concat( args, sep or '' ) end --[[ Helper function that populates the argument list given that user may need to use a mix of named and unnamed parameters. This is relevant because named parameters are not identical to unnamed parameters due to string trimming, and when dealing with strings we sometimes want to either preserve or remove that whitespace depending on the application. ]] function str._getParameters( frame_args, arg_list ) local new_args = {} local index = 1 local value for _, arg in ipairs( arg_list ) do value = frame_args[arg] if value == nil then value = frame_args[index] index = index + 1 end new_args[arg] = value end return new_args end --[[ Helper function to handle error messages. ]] function str._error( error_str ) local frame = mw.getCurrentFrame() local error_category = frame.args.error_category or 'Errors reported by Module String' local ignore_errors = frame.args.ignore_errors or false local no_category = frame.args.no_category or false if str._getBoolean(ignore_errors) then return '' end local error_str = '<strong class="error">String Module Error: ' .. error_str .. '</strong>' if error_category ~= '' and not str._getBoolean( no_category ) then error_str = '[[Category:' .. error_category .. ']]' .. error_str end return error_str end --[[ Helper Function to interpret boolean strings ]] function str._getBoolean( boolean_str ) local boolean_value if type( boolean_str ) == 'string' then boolean_str = boolean_str:lower() if boolean_str == 'false' or boolean_str == 'no' or boolean_str == '0' or boolean_str == '' then boolean_value = false else boolean_value = true end elseif type( boolean_str ) == 'boolean' then boolean_value = boolean_str else error( 'No boolean value found' ) end return boolean_value end --[[ Helper function that escapes all pattern characters so that they will be treated as plain text. ]] function str._escapePattern( pattern_str ) return mw.ustring.gsub( pattern_str, "([%(%)%.%%%+%-%*%?%[%^%$%]])", "%%%1" ) end return str 73c9d229ca32cb5e05a3873238b69fec347cf4b1 Module:Userbox 828 94 181 180 2023-08-28T02:15:30Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain -- This module implements {{userbox}}. local p = {} -------------------------------------------------------------------------------- -- Helper functions -------------------------------------------------------------------------------- local function checkNum(val, default) -- Checks whether a value is a number greater than or equal to zero. If so, -- returns it as a number. If not, returns a default value. val = tonumber(val) if val and val >= 0 then return val else return default end end local function addSuffix(num, suffix) -- Turns a number into a string and adds a suffix. if num then return tostring(num) .. suffix else return nil end end local function checkNumAndAddSuffix(num, default, suffix) -- Checks a value with checkNum and adds a suffix. num = checkNum(num, default) return addSuffix(num, suffix) end local function makeCat(cat, sort) -- Makes a category link. if sort then return mw.ustring.format('[[Category:%s|%s]]', cat, sort) else return mw.ustring.format('[[Category:%s]]', cat) end end -------------------------------------------------------------------------------- -- Argument processing -------------------------------------------------------------------------------- local function makeInvokeFunc(funcName) return function (frame) local origArgs = require('Module:Arguments').getArgs(frame) local args = {} for k, v in pairs(origArgs) do args[k] = v end return p.main(funcName, args) end end p.userbox = makeInvokeFunc('_userbox') p['userbox-2'] = makeInvokeFunc('_userbox-2') p['userbox-r'] = makeInvokeFunc('_userbox-r') -------------------------------------------------------------------------------- -- Main functions -------------------------------------------------------------------------------- function p.main(funcName, args) local userboxData = p[funcName](args) local userbox = p.render(userboxData) local cats = p.categories(args) return userbox .. (cats or '') end function p._userbox(args) -- Does argument processing for {{userbox}}. local data = {} -- Get div tag values. data.float = args.float or 'left' local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width. data.borderWidth = addSuffix(borderWidthNum, 'px') data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999' data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag. data.bodyClass = args.bodyclass -- Get table tag values. data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee' -- Get info values. data.info = args.info or args[4] or "<code>{{{info}}}</code>" data.infoTextAlign = args['info-a'] or 'left' data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt') data.infoHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px') data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px' data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em' data.infoColor = args['info-color'] or args['info-fc'] or 'black' data.infoOtherParams = args['info-other-param'] or args['info-op'] data.infoClass = args['info-class'] -- Get id values. local id = args.logo or args[3] or args.id data.id = id data.showId = id and true or false data.idWidth = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px') data.idHeight = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px') data.idBackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd' data.idTextAlign = args['id-a'] or 'center' data.idFontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt') data.idColor = args['logo-color'] or args['id-fc'] or data.infoColor data.idPadding = args['logo-padding'] or args['id-p'] or '0 1px 0 0' data.idLineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em' data.idOtherParams = args['logo-other-param'] or args['id-op'] data.idClass = args['id-class'] return data end p['_userbox-2'] = function (args) -- Does argument processing for {{userbox-2}}. local data = {} -- Get div tag values. data.float = args.float or 'left' local borderWidthNum = checkNum(args[9] or args['border-s'], 1) -- Used to calculate width. data.borderWidth = addSuffix(borderWidthNum, 'px') data.borderColor = args[1] or args['border-c'] or args['id1-c'] or '#999999' data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag. data.bodyClass = args.bodyclass -- Get table tag values. data.backgroundColor = args[2] or args['info-c'] or '#eeeeee' -- Get info values. data.info = args[4] or args.info or "<code>{{{info}}}</code>" data.infoTextAlign = args['info-a'] or 'left' data.infoFontSize = checkNumAndAddSuffix(args['info-s'], 8, 'pt') data.infoColor = args[8] or args['info-fc'] or 'black' data.infoPadding = args['info-p'] or '0 4px 0 4px' data.infoLineHeight = args['info-lh'] or '1.25em' data.infoOtherParams = args['info-op'] -- Get id values. data.showId = true data.id = args.logo or args[3] or args.id1 or 'id1' data.idWidth = checkNumAndAddSuffix(args['id1-w'], 45, 'px') data.idHeight = checkNumAndAddSuffix(args['id-h'], 45, 'px') data.idBackgroundColor = args[1] or args['id1-c'] or '#dddddd' data.idTextAlign = 'center' data.idFontSize = checkNumAndAddSuffix(args['id1-s'], 14, 'pt') data.idLineHeight = args['id1-lh'] or '1.25em' data.idColor = args['id1-fc'] or data.infoColor data.idPadding = args['id1-p'] or '0 1px 0 0' data.idOtherParams = args['id1-op'] -- Get id2 values. data.showId2 = true data.id2 = args.logo or args[5] or args.id2 or 'id2' data.id2Width = checkNumAndAddSuffix(args['id2-w'], 45, 'px') data.id2Height = data.idHeight data.id2BackgroundColor = args[7] or args['id2-c'] or args[1] or '#dddddd' data.id2TextAlign = 'center' data.id2FontSize = checkNumAndAddSuffix(args['id2-s'], 14, 'pt') data.id2LineHeight = args['id2-lh'] or '1.25em' data.id2Color = args['id2-fc'] or data.infoColor data.id2Padding = args['id2-p'] or '0 0 0 1px' data.id2OtherParams = args['id2-op'] return data end p['_userbox-r'] = function (args) -- Does argument processing for {{userbox-r}}. local data = {} -- Get div tag values. data.float = args.float or 'left' local borderWidthNum = checkNum(args['border-width'] or args['border-s'], 1) -- Used to calculate width. data.borderWidth = addSuffix(borderWidthNum, 'px') data.borderColor = args['border-color'] or args[1] or args['border-c'] or args['id-c'] or '#999' data.width = addSuffix(240 - 2 * borderWidthNum, 'px') -- Also used in the table tag. data.bodyClass = args.bodyclass -- Get table tag values. data.backgroundColor = args['info-background'] or args[2] or args['info-c'] or '#eee' -- Get id values. data.showId = false -- We only show id2 in userbox-r. -- Get info values. data.info = args.info or args[4] or "<code>{{{info}}}</code>" data.infoTextAlign = args['info-align'] or args['info-a'] or 'left' data.infoFontSize = checkNumAndAddSuffix(args['info-size'] or args['info-s'], 8, 'pt') data.infoPadding = args['info-padding'] or args['info-p'] or '0 4px 0 4px' data.infoLineHeight = args['info-line-height'] or args['info-lh'] or '1.25em' data.infoColor = args['info-color'] or args['info-fc'] or 'black' data.infoOtherParams = args['info-other-param'] or args['info-op'] -- Get id2 values. data.showId2 = true data.id2 = args.logo or args[3] or args.id or 'id' data.id2Width = checkNumAndAddSuffix(args['logo-width'] or args['id-w'], 45, 'px') data.id2Height = checkNumAndAddSuffix(args['logo-height'] or args['id-h'], 45, 'px') data.id2BackgroundColor = args['logo-background'] or args[1] or args['id-c'] or '#ddd' data.id2TextAlign = args['id-a'] or 'center' data.id2FontSize = checkNumAndAddSuffix(args['logo-size'] or args[5] or args['id-s'], 14, 'pt') data.id2Color = args['logo-color'] or args['id-fc'] or data.infoColor data.id2Padding = args['logo-padding'] or args['id-p'] or '0 0 0 1px' data.id2LineHeight = args['logo-line-height'] or args['id-lh'] or '1.25em' data.id2OtherParams = args['logo-other-param'] or args['id-op'] return data end function p.render(data) -- Renders the userbox html using the content of the data table. -- Render the div tag html. local root = mw.html.create('div') root :css('float', data.float) :css('border', (data.borderWidth or '') .. ' solid ' .. (data.borderColor or '')) :css('margin', '1px') :css('width', data.width) :addClass('wikipediauserbox') :addClass(data.bodyClass) -- Render the table tag html. local tableroot = root:tag('table') tableroot :css('border-collapse', 'collapse') :css('width', data.width) :css('margin-bottom', '0') :css('margin-top', '0') :css('background', data.backgroundColor) -- Render the id html. local tablerow = tableroot:tag('tr') if data.showId then tablerow:tag('th') :css('border', '0') :css('width', data.idWidth) :css('height', data.idHeight) :css('background', data.idBackgroundColor) :css('text-align', data.idTextAlign) :css('font-size', data.idFontSize) :css('color', data.idColor) :css('padding', data.idPadding) :css('line-height', data.idLineHeight) :css('vertical-align', 'middle') :cssText(data.idOtherParams) :addClass(data.idClass) :wikitext(data.id) end -- Render the info html. tablerow:tag('td') :css('border', '0') :css('text-align', data.infoTextAlign) :css('font-size', data.infoFontSize) :css('padding', data.infoPadding) :css('height', data.infoHeight) :css('line-height', data.infoLineHeight) :css('color', data.infoColor) :css('vertical-align', 'middle') :cssText(data.infoOtherParams) :addClass(data.infoClass) :wikitext(data.info) -- Render the second id html. if data.showId2 then tablerow:tag('th') :css('border', '0') :css('width', data.id2Width) :css('height', data.id2Height) :css('background', data.id2BackgroundColor) :css('text-align', data.id2TextAlign) :css('font-size', data.id2FontSize) :css('color', data.id2Color) :css('padding', data.id2Padding) :css('line-height', data.id2LineHeight) :css('vertical-align', 'middle') :cssText(data.id2OtherParams) :wikitext(data.id2) end local title = mw.title.getCurrentTitle() if (title.namespace == 2) and not title.text:match("/") then return tostring(root) -- regular user page elseif title.namespace == 14 then return tostring(root) -- category elseif title.isTalkPage then return tostring(root) -- talk page end local function has_text(wikitext) local function get_alt(text) return text:match("|alt=([^|]*)") or "" end wikitext = wikitext:gsub("]]", "|]]") wikitext = wikitext:gsub("%[%[%s*[Mm][Ee][Dd][Ii][Aa]%s*:[^|]-(|.-)]]", get_alt) wikitext = wikitext:gsub("%[%[%s*[Ii][Mm][Aa][Gg][Ee]%s*:[^|]-(|.-)]]", get_alt) wikitext = wikitext:gsub("%[%[%s*[Ff][Ii][Ll][Ee]%s*:[^|]-(|.-)]]", get_alt) return mw.text.trim(wikitext) ~= "" end return tostring(root) end function p.categories(args, page) -- Gets categories -- The page parameter makes the function act as though the module was being called from that page. -- It is included for testing purposes. local cats = {} cats[#cats + 1] = args.usercategory cats[#cats + 1] = args.usercategory2 cats[#cats + 1] = args.usercategory3 if #cats > 0 and not require("Module:Yesno")(args.nocat) then -- Get the title object local title if page then title = mw.title.new(page) else title = mw.title.getCurrentTitle() end -- Build category handler arguments. local chargs = {} chargs.page = page chargs.main = '[[Category:Pages with misplaced templates]]' if title.namespace == 2 then -- User namespace. local user = '' for i, cat in ipairs(cats) do user = user .. makeCat(cat) end return user elseif title.namespace == 10 then -- Template namespace. local basepage = title.baseText local template = '' for i, cat in ipairs(cats) do template = template .. makeCat(cat, ' ' .. basepage) end return template end end end return p aac333efff739f0243d8ffced6f4296cffb8d7e9 Module:Yesno 828 95 183 182 2023-08-28T02:15:30Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 Scribunto text/plain -- Function allowing for consistent treatment of boolean-like wikitext input. -- It works similarly to the template {{yesno}}. return function (val, default) -- If your wiki uses non-ascii characters for any of "yes", "no", etc., you -- should replace "val:lower()" with "mw.ustring.lower(val)" in the -- following line. val = type(val) == 'string' and val:lower() or val if val == nil then return nil elseif val == true or val == 'yes' or val == 'y' or val == 'true' or val == 't' or val == 'on' or tonumber(val) == 1 then return true elseif val == false or val == 'no' or val == 'n' or val == 'false' or val == 'f' or val == 'off' or tonumber(val) == 0 then return false else return default end end f767643e7d12126d020d88d662a3dd057817b9dc File:Logo.svg 6 96 184 2023-08-28T02:27:29Z Alxira5 4 Tux the Penguipedia for your wiki and social networks. wikitext text/x-wiki == Summary == Tux the Penguipedia for your wiki and social networks. 63a5089ad64bfa6e59c6e6715d0e1e494fa80b7d Module:Documentation/styles.css 828 97 186 185 2023-08-29T02:59:04Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license text text/plain .documentation, .documentation-metadata { border: 1px solid #a2a9b1; background-color: #ecfcf4; clear: both; } .documentation { margin: 1em 0 0 0; padding: 1em; } .documentation-metadata { margin: 0.2em 0; /* same margin left-right as .documentation */ font-style: italic; padding: 0.4em 1em; /* same padding left-right as .documentation */ } .documentation-startbox { padding-bottom: 3px; border-bottom: 1px solid #aaa; margin-bottom: 1ex; } .documentation-heading { font-weight: bold; font-size: 125%; } .documentation-clear { /* Don't want things to stick out where they shouldn't. */ clear: both; } .documentation-toolbar { font-style: normal; font-size: 85%; } /* [[Category:Template stylesheets]] */ 5fb984fe8632dc068db16853a824c9f3d5175dd9 Template:If empty 10 98 188 187 2023-08-29T02:59:04Z Alxira5 4 1 revision imported: Modules and templates imported from Miraheze Developers, published under the CC-BY-SA 4.0 license wikitext text/x-wiki <includeonly>{{{{{|safesubst:}}}#if:{{{1|}}} | {{{1}}} | {{{{{|safesubst:}}}#if:{{{2|}}} | {{{2}}} | {{{{{|safesubst:}}}#if:{{{3|}}} | {{{3}}} | {{{{{|safesubst:}}}#if:{{{4|}}} | {{{4}}} | {{{{{|safesubst:}}}#if:{{{5|}}} | {{{5}}} | {{{{{|safesubst:}}}#if:{{{6|}}} | {{{6}}} | {{{{{|safesubst:}}}#if:{{{7|}}} | {{{7}}} | {{{{{|safesubst:}}}#if:{{{8|}}} | {{{8}}} | {{{{{|safesubst:}}}#if:{{{9|}}} | {{{9}}} }} }} }} }} }} }} }} }} }}</includeonly><noinclude> {{Documentation}} <!-- Add categories and interwikis to the /doc subpage, not here! --> </noinclude> eeda2c13231e9a8b44d480e8c429d73652575009 Tux the Penguipedia:Code of conduct 4 99 189 2023-08-29T04:30:22Z Alxira5 4 Create the encyclopedia code of conduct page wikitext text/x-wiki {{header | title = Code of conduct | notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content. | bodyhex = 1accf0 }} == Contributor Covenant Code of Conduct == === Our Pledge === We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. === Our Standards === Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others’ private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting === Enforcement Responsibilities === Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. === Scope === This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. === Enforcement === Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at {[Tux the Penguipedia:Contact]]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. === Enforcement Guidelines === Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ==== 1. Correction ==== '''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. '''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ==== 2. Warning ==== '''Community Impact''': A violation through a single incident or series of actions. '''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ==== 3. Temporary Ban ==== '''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior. '''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ==== 4. Permanent Ban ==== '''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. '''Consequence''': A permanent ban from any sort of public interaction within the community. === Attribution === This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder]. For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations. [[Category:Documentation]] 715ac70e161fa41223bae3bebab0e32611c8095e 190 189 2023-08-29T04:33:19Z Alxira5 4 Protected "[[Tux the Penguipedia:Code of conduct]]": [object Object]Protect this page to avoid vandalism ([Edit=Allow only administrators] (indefinite)) wikitext text/x-wiki {{header | title = Code of conduct | notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content. | bodyhex = 1accf0 }} == Contributor Covenant Code of Conduct == === Our Pledge === We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. === Our Standards === Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others’ private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting === Enforcement Responsibilities === Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. === Scope === This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. === Enforcement === Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at {[Tux the Penguipedia:Contact]]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. === Enforcement Guidelines === Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ==== 1. Correction ==== '''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. '''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ==== 2. Warning ==== '''Community Impact''': A violation through a single incident or series of actions. '''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ==== 3. Temporary Ban ==== '''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior. '''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ==== 4. Permanent Ban ==== '''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. '''Consequence''': A permanent ban from any sort of public interaction within the community. === Attribution === This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder]. For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations. [[Category:Documentation]] 715ac70e161fa41223bae3bebab0e32611c8095e 193 190 2023-08-29T16:31:41Z Alxira5 4 I added the color of the top bar wikitext text/x-wiki {{header | title = Code of conduct | notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content. | topbarhex = 118da6 | bodyhex = 1accf0 }} == Contributor Covenant Code of Conduct == === Our Pledge === We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. === Our Standards === Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others’ private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting === Enforcement Responsibilities === Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. === Scope === This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. === Enforcement === Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at {[Tux the Penguipedia:Contact]]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. === Enforcement Guidelines === Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ==== 1. Correction ==== '''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. '''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ==== 2. Warning ==== '''Community Impact''': A violation through a single incident or series of actions. '''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ==== 3. Temporary Ban ==== '''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior. '''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ==== 4. Permanent Ban ==== '''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. '''Consequence''': A permanent ban from any sort of public interaction within the community. === Attribution === This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder]. For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations. [[Category:Documentation]] 80f036d60555d6331b91da13f66787a724c59292 194 193 2023-08-29T23:28:33Z Alxira5 4 Fixed the contact link and changed the header color to make it more visible in Citizen's dark theme wikitext text/x-wiki {{header | title = Code of conduct | notes = If you want to contribute to this encyclopedia, you must follow the code of conduct and agree with the [[Tux the Penguipedia:Copyrights|license]] of the content. }} == Contributor Covenant Code of Conduct == === Our Pledge === We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. === Our Standards === Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others’ private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting === Enforcement Responsibilities === Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. === Scope === This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. === Enforcement === Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [[User talk:Alxira5|Alxira5]]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. === Enforcement Guidelines === Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ==== 1. Correction ==== '''Community Impact''': Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. '''Consequence''': A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ==== 2. Warning ==== '''Community Impact''': A violation through a single incident or series of actions. '''Consequence''': A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ==== 3. Temporary Ban ==== '''Community Impact''': A serious violation of community standards, including sustained inappropriate behavior. '''Consequence''': A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ==== 4. Permanent Ban ==== '''Community Impact''': Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. '''Consequence''': A permanent ban from any sort of public interaction within the community. === Attribution === This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. Community Impact Guidelines were inspired by [https://github.com/mozilla/diversity Mozilla’s code of conduct enforcement ladder]. For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq]. Translations are available at https://www.contributor-covenant.org/translations. [[Category:Documentation]] af539569987c56eca9337631582d71f50d58ed7a Category:Documentation 14 100 191 2023-08-29T05:50:19Z Alxira5 4 Add the information about the category wikitext text/x-wiki In this category are the articles of the Tux the Penguipedia documentation. For more information, visit [[Tux the Penguipedia:Documentation]]. 1aa14f06044a1e76206247bddd32707d8c855aaf 192 191 2023-08-29T05:51:56Z Alxira5 4 Protected "[[Category:Documentation]]": nullProtect this category to avoid vandalism ([Edit=Allow only administrators] (indefinite)) wikitext text/x-wiki In this category are the articles of the Tux the Penguipedia documentation. For more information, visit [[Tux the Penguipedia:Documentation]]. 1aa14f06044a1e76206247bddd32707d8c855aaf MediaWiki:Citizen-footer-tagline 8 101 195 2023-08-29T23:37:08Z Alxira5 4 Create encyclopedia social links wikitext text/x-wiki [https://mastodon.online/@TuxthePenguipedia {{#fab:mastodon}}] [irc://irc.libera.chat/#tuxthepenguipedia {{#fas:comment}}] e9e2054bee8d789e508023c365fe34b4741470ed User:AlXGX21 2 14 196 23 2023-11-16T01:48:46Z Void 5 Void moved page [[User:AlexGX22]] to [[User:AlXGX21]]: Automatically moved page while renaming the user "[[Special:CentralAuth/AlexGX22|AlexGX22]]" to "[[Special:CentralAuth/AlXGX21|AlXGX21]]" wikitext text/x-wiki #REDIRECT [[User:Alxira5|</nowiki>Alxira5]] 5cd0368e4dea8043dc1782a4946317e31e03d2a2 User:AlexGX22 2 102 197 2023-11-16T01:48:52Z Void 5 Void moved page [[User:AlexGX22]] to [[User:AlXGX21]]: Automatically moved page while renaming the user "[[Special:CentralAuth/AlexGX22|AlexGX22]]" to "[[Special:CentralAuth/AlXGX21|AlXGX21]]" wikitext text/x-wiki #REDIRECT [[User:AlXGX21]] a5424424e8c4d8ed9e35757f04b9f769735a5abe